Compare commits
133 Commits
Author | SHA1 | Date |
---|---|---|
Vitaliy Filippov | 655d27d543 | |
Vitaliy Filippov | ce04ad9344 | |
Vitaliy Filippov | bbcb30140c | |
Vitaliy Filippov | ae264bdf2d | |
Vitaliy Filippov | 5e83139625 | |
Vitaliy Filippov | cb0f6d025c | |
Vitaliy Filippov | 02e716777f | |
Vitaliy Filippov | d3257ad482 | |
Vitaliy Filippov | d31c230159 | |
Vitaliy Filippov | 7c13c5e36e | |
Vitaliy Filippov | 935805170e | |
Vitaliy Filippov | 4212177a2d | |
Vitaliy Filippov | 06fc8350ac | |
Vitaliy Filippov | 05cd2beaf8 | |
Vitaliy Filippov | d6f3c1c869 | |
Vitaliy Filippov | 64341d027c | |
Vitaliy Filippov | 08c91c0007 | |
Vitaliy Filippov | 374efe3279 | |
Vitaliy Filippov | 0191669861 | |
Vitaliy Filippov | bf83ef7a98 | |
Vitaliy Filippov | 84dfe3918c | |
Vitaliy Filippov | 4b7a51efa8 | |
Vitaliy Filippov | a4f77c4340 | |
Vitaliy Filippov | 3f66ba7490 | |
Vitaliy Filippov | 7683087937 | |
Vitaliy Filippov | f9545db73b | |
Vitaliy Filippov | d538552033 | |
Vitaliy Filippov | 0c553e6a0e | |
Vitaliy Filippov | d34758d028 | |
Vitaliy Filippov | 3e36282959 | |
Vitaliy Filippov | 861ce067d9 | |
Vitaliy Filippov | 94d624b036 | |
Vitaliy Filippov | fe7734fbc6 | |
Vitaliy Filippov | 4ac55c1121 | |
Vitaliy Filippov | a1e873904f | |
Vitaliy Filippov | 65e825d471 | |
Vitaliy Filippov | 3b9ba2b7b5 | |
Vitaliy Filippov | 5f396a2d77 | |
Vitaliy Filippov | e395f89f73 | |
Vitaliy Filippov | 0122af7a78 | |
Vitaliy Filippov | c8b8ccd383 | |
Vitaliy Filippov | fcc7be34af | |
Vitaliy Filippov | 669b67215b | |
Vitaliy Filippov | 155e56dfcb | |
Vitaliy Filippov | 257d4b0ebb | |
Vitaliy Filippov | d63bc27928 | |
Vitaliy Filippov | 349af79c2e | |
Vitaliy Filippov | 917f3e1566 | |
Vitaliy Filippov | e08c5f3a6b | |
Vitaliy Filippov | 9f6262a6e3 | |
Vitaliy Filippov | f7321af1b1 | |
Vitaliy Filippov | 4ae73720a8 | |
Vitaliy Filippov | a8c63de19d | |
Vitaliy Filippov | 97aa1f1787 | |
Vitaliy Filippov | 7802af4bc9 | |
Vitaliy Filippov | 3a9b5f93f5 | |
Vitaliy Filippov | a7609559c5 | |
Vitaliy Filippov | ba08f1f46d | |
Vitaliy Filippov | a608641a2b | |
Vitaliy Filippov | ca2014f6ce | |
Vitaliy Filippov | 6a9039b06e | |
Vitaliy Filippov | ae75b4e493 | |
Vitaliy Filippov | 55bee6cdcc | |
Vitaliy Filippov | 13f6cca220 | |
Vitaliy Filippov | 45686f41a3 | |
Vitaliy Filippov | a8e11f918e | |
Vitaliy Filippov | ad5c5b6814 | |
Vitaliy Filippov | 76be373f40 | |
Vitaliy Filippov | d2646d08b5 | |
Vitaliy Filippov | 1db3507cbf | |
Vitaliy Filippov | e63bc93fec | |
Vitaliy Filippov | 7bf84a131b | |
Vitaliy Filippov | 41cf047168 | |
Vitaliy Filippov | 4b8bea7b17 | |
Vitaliy Filippov | 8e13a0ff60 | |
Vitaliy Filippov | 3efbc41166 | |
Vitaliy Filippov | 885419afba | |
Vitaliy Filippov | c91aaaf113 | |
Vitaliy Filippov | 25a101df9c | |
Vitaliy Filippov | 824a39678d | |
Vitaliy Filippov | 7153084880 | |
Vitaliy Filippov | 003913e3fc | |
Vitaliy Filippov | 0bc3e2599c | |
Vitaliy Filippov | 3ade1d0a8d | |
Vitaliy Filippov | ea2dfa1fe4 | |
Vitaliy Filippov | 83202248a4 | |
Vitaliy Filippov | b8f096bd42 | |
Vitaliy Filippov | a6ec66012c | |
Vitaliy Filippov | ca5dde4d80 | |
Vitaliy Filippov | 4736a51e18 | |
Vitaliy Filippov | 4ad3e1d881 | |
Vitaliy Filippov | 98b88de635 | |
Vitaliy Filippov | 5b4ce0bd96 | |
Vitaliy Filippov | cedb870a81 | |
Vitaliy Filippov | b37cc0c1b3 | |
Vitaliy Filippov | 3e789c2a51 | |
Vitaliy Filippov | d460b74b8c | |
Vitaliy Filippov | 40dc73f692 | |
Vitaliy Filippov | 40251a927a | |
Vitaliy Filippov | 823c7afe49 | |
Vitaliy Filippov | a718d113a6 | |
Vitaliy Filippov | 8d9eb2b89a | |
Vitaliy Filippov | 35df417ee1 | |
Vitaliy Filippov | a7bf3751bd | |
Vitaliy Filippov | e760a23ae2 | |
Vitaliy Filippov | 6e9ee91dcf | |
Vitaliy Filippov | bd69dca4a1 | |
Vitaliy Filippov | 2412e88f67 | |
Vitaliy Filippov | 240180bb05 | |
Vitaliy Filippov | 773d33e561 | |
Vitaliy Filippov | 0252410899 | |
Vitaliy Filippov | 57f6b13dd5 | |
Vitaliy Filippov | 142a9561b2 | |
Vitaliy Filippov | 673aada44e | |
Vitaliy Filippov | 6f1bb58668 | |
Vitaliy Filippov | 711952b34c | |
Vitaliy Filippov | 3e4cd49b2c | |
Vitaliy Filippov | 58346deb00 | |
Vitaliy Filippov | cb5b2f06e0 | |
Vitaliy Filippov | a51d9a9deb | |
Vitaliy Filippov | 805d0afc82 | |
Vitaliy Filippov | 799a177c31 | |
Vitaliy Filippov | 9be26744ed | |
Vitaliy Filippov | f39407bcab | |
Vitaliy Filippov | 801b4312e8 | |
Vitaliy Filippov | 10988be74f | |
Vitaliy Filippov | 594e5ee476 | |
Vitaliy Filippov | 3d748e36ec | |
Vitaliy Filippov | 400a4b1bed | |
Vitaliy Filippov | e34e9158fb | |
Vitaliy Filippov | bcec6967ee | |
Vitaliy Filippov | a3ffc5a247 | |
Vitaliy Filippov | 5d7747401f |
|
@ -737,11 +737,6 @@ sub validate_obsolete
|
|||
ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
|
||||
}
|
||||
|
||||
if ($attachment->isobsolete)
|
||||
{
|
||||
ThrowCodeError('attachment_already_obsolete', $vars);
|
||||
}
|
||||
|
||||
push(@obsolete_attachments, $attachment);
|
||||
}
|
||||
return @obsolete_attachments;
|
||||
|
|
128
Bugzilla/Bug.pm
128
Bugzilla/Bug.pm
|
@ -106,8 +106,13 @@ sub DB_COLUMNS
|
|||
# FIXME kill op_sys and rep_platform completely, make them custom fields
|
||||
push @columns, 'op_sys' if Bugzilla->get_field('op_sys')->enabled;
|
||||
push @columns, 'rep_platform' if Bugzilla->get_field('rep_platform')->enabled;
|
||||
push @columns, map { $_->name }
|
||||
grep { $_->type != FIELD_TYPE_MULTI_SELECT && $_->type != FIELD_TYPE_BUG_ID_REV }
|
||||
push @columns,
|
||||
map { $_->name }
|
||||
grep {
|
||||
$_->type != FIELD_TYPE_MULTI_SELECT &&
|
||||
$_->type != FIELD_TYPE_EAV_TEXTAREA &&
|
||||
$_->type != FIELD_TYPE_BUG_ID_REV
|
||||
}
|
||||
Bugzilla->active_custom_fields;
|
||||
|
||||
Bugzilla::Hook::process('bug_columns', { columns => \@columns });
|
||||
|
@ -162,6 +167,7 @@ use constant CUSTOM_FIELD_VALIDATORS => {
|
|||
FIELD_TYPE_BUG_ID_REV() => \&_set_bugid_rev_field,
|
||||
FIELD_TYPE_BUG_URLS() => \&_set_default_field,
|
||||
FIELD_TYPE_NUMERIC() => \&_set_numeric_field,
|
||||
FIELD_TYPE_EAV_TEXTAREA() => \&_set_default_field,
|
||||
};
|
||||
|
||||
sub SETTERS
|
||||
|
@ -353,6 +359,17 @@ sub check_exists
|
|||
return $self;
|
||||
}
|
||||
|
||||
sub check_for_edit
|
||||
{
|
||||
my $class = shift;
|
||||
my $bug = $class->check(@_);
|
||||
|
||||
Bugzilla->user->can_edit_product($bug->product_id)
|
||||
|| ThrowUserError("product_edit_denied", { product => $bug->product });
|
||||
|
||||
return $bug;
|
||||
}
|
||||
|
||||
# Check if a bug exists and is visible for the current user or throw an error
|
||||
sub check
|
||||
{
|
||||
|
@ -465,6 +482,8 @@ sub create
|
|||
sub update
|
||||
{
|
||||
my $self = shift;
|
||||
my ($delta_ts, $additional_changes) = @_;
|
||||
|
||||
$self->make_dirty;
|
||||
|
||||
my $method = $self->id ? 'update' : 'create';
|
||||
|
@ -476,10 +495,10 @@ sub update
|
|||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
# FIXME 'shift ||' is just a temporary hack until all updating happens inside this function
|
||||
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
$delta_ts = $delta_ts || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
|
||||
|
||||
# You can't set these fields by hand
|
||||
$self->{deadline} =~ s/\s+.*$//so;
|
||||
$self->{delta_ts} = $delta_ts;
|
||||
delete $self->{votes};
|
||||
delete $self->{lastdiffed};
|
||||
|
@ -498,7 +517,8 @@ sub update
|
|||
|
||||
if ($self->id)
|
||||
{
|
||||
($changes, $old_bug) = $self->SUPER::update(@_);
|
||||
$old_bug->{deadline} =~ s/\s+.*$//so;
|
||||
($changes, $old_bug) = $self->SUPER::update(undef, $old_bug);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -562,6 +582,9 @@ sub update
|
|||
# Insert the values into the multiselect value tables
|
||||
$self->save_multiselects($changes);
|
||||
|
||||
# Insert the values into satellite value tables
|
||||
$self->save_eavs($changes);
|
||||
|
||||
# Save reversed bug_id fields
|
||||
$self->save_reverse_bugid_fields($changes);
|
||||
|
||||
|
@ -605,6 +628,10 @@ sub update
|
|||
$self->prepare_mail_results($changes);
|
||||
|
||||
# Remove obsolete internal variables.
|
||||
if ($additional_changes)
|
||||
{
|
||||
$additional_changes->{added_comments} = $self->{added_comments};
|
||||
}
|
||||
delete $self->{_old_self};
|
||||
delete $self->{added_comments};
|
||||
delete $self->{edited_comments};
|
||||
|
@ -749,7 +776,7 @@ sub check_default_values
|
|||
push @gids, $gid;
|
||||
}
|
||||
}
|
||||
$self->{groups_in} = \@gids;
|
||||
$self->{groups_in} = Bugzilla::Group->new_from_list(\@gids);
|
||||
}
|
||||
$self->set('groups', [ map { $_->id } @{$self->groups_in} ]);
|
||||
$self->{cc} = $self->component_obj->initial_cc if !$self->{cc};
|
||||
|
@ -914,6 +941,7 @@ sub check_dependent_fields
|
|||
}
|
||||
# Check other fields for empty values
|
||||
elsif (!$self->{$fn} || ($field_obj->type == FIELD_TYPE_FREETEXT ||
|
||||
$field_obj->type == FIELD_TYPE_EAV_TEXTAREA ||
|
||||
$field_obj->type == FIELD_TYPE_TEXTAREA) && $self->{$fn} =~ /^\s*$/so)
|
||||
{
|
||||
my $nullable = $field_obj->check_is_nullable($self);
|
||||
|
@ -927,6 +955,7 @@ sub check_dependent_fields
|
|||
}
|
||||
}
|
||||
if (!$nullable && (!$self->{$fn} || ($field_obj->type == FIELD_TYPE_FREETEXT ||
|
||||
$field_obj->type == FIELD_TYPE_EAV_TEXTAREA ||
|
||||
$field_obj->type == FIELD_TYPE_TEXTAREA) && $self->{$fn} =~ /^\s*$/so))
|
||||
{
|
||||
ThrowUserError('field_not_nullable', { field => $field_obj });
|
||||
|
@ -1078,14 +1107,6 @@ sub _check_bug_status
|
|||
}
|
||||
$self->set('resolution', undef);
|
||||
}
|
||||
else
|
||||
{
|
||||
# Changing between closed statuses zeroes the remaining time.
|
||||
if ($old_status && $new_status->id != $old_status->id && $self->remaining_time != 0)
|
||||
{
|
||||
$self->set('remaining_time', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _check_resolution
|
||||
|
@ -1439,6 +1460,31 @@ sub save_multiselects
|
|||
}
|
||||
}
|
||||
|
||||
sub save_eavs
|
||||
{
|
||||
my ($self, $changes) = @_;
|
||||
|
||||
my @eavs = Bugzilla->get_fields({ obsolete => 0, type => FIELD_TYPE_EAV_TEXTAREA });
|
||||
foreach my $field (@eavs)
|
||||
{
|
||||
my $name = $field->name;
|
||||
if (defined $self->{$name})
|
||||
{
|
||||
my $old = $self->{_old_self} && $self->{_old_self}->$name || '';
|
||||
my $v = $self->{$name};
|
||||
if ($v ne $old)
|
||||
{
|
||||
Bugzilla->dbh->do("DELETE FROM bug_$name WHERE bug_id=?", undef, $self->id);
|
||||
if ($v ne '')
|
||||
{
|
||||
Bugzilla->dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?, ?)", undef, $self->id, $v);
|
||||
}
|
||||
$changes->{$name} = [ $old, $v ];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub save_see_also
|
||||
{
|
||||
my ($self, $changes) = @_;
|
||||
|
@ -1486,7 +1532,7 @@ sub save_dup_id
|
|||
sub save_added_comments
|
||||
{
|
||||
my ($self, $changes) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
delete $self->{comments} if @{$self->{added_comments} || []};
|
||||
foreach my $comment (@{$self->{added_comments} || []})
|
||||
{
|
||||
|
@ -1502,7 +1548,8 @@ sub save_added_comments
|
|||
$comment->{bug_when} = $self->{delta_ts} if !$comment->{bug_when} || $comment->{bug_when} gt $self->{delta_ts};
|
||||
my $columns = join(',', keys %$comment);
|
||||
my $qmarks = join(',', ('?') x keys %$comment);
|
||||
Bugzilla->dbh->do("INSERT INTO longdescs ($columns) VALUES ($qmarks)", undef, values %$comment);
|
||||
$dbh->do("INSERT INTO longdescs ($columns) VALUES ($qmarks)", undef, values %$comment);
|
||||
$comment->{id} = $dbh->bz_last_key('longdescs', 'id');
|
||||
if (0+$comment->{work_time} != 0)
|
||||
{
|
||||
# Log worktime
|
||||
|
@ -1675,7 +1722,9 @@ sub _sync_fulltext
|
|||
$sql = "UPDATE $table SET short_desc=$row->[0],".
|
||||
" comments=$row->[1], comments_private=$row->[2] WHERE $id_field=".$self->id;
|
||||
}
|
||||
return $sph->do($sql);
|
||||
my $r = eval { $sph->do($sql) };
|
||||
if ($@) { warn $@; }
|
||||
return $r;
|
||||
}
|
||||
|
||||
# This is the correct way to delete bugs from the DB.
|
||||
|
@ -1903,7 +1952,11 @@ sub _set_deadline
|
|||
|
||||
# Validate entered deadline
|
||||
$date = trim($date);
|
||||
return undef if !$date;
|
||||
if (!$date || $date =~ /^0000-00-00/)
|
||||
{
|
||||
$self->{deadline} = undef;
|
||||
return undef;
|
||||
}
|
||||
|
||||
$date =~ s/\s+.*//s;
|
||||
validate_date($date) || ThrowUserError('illegal_date', { date => $date, format => 'YYYY-MM-DD' });
|
||||
|
@ -2150,6 +2203,9 @@ sub _set_keywords
|
|||
sub _set_product
|
||||
{
|
||||
my ($self, $name) = @_;
|
||||
my $product;
|
||||
if (!ref $name)
|
||||
{
|
||||
$name = trim($name);
|
||||
# If we're updating the bug and they haven't changed the product, always allow it.
|
||||
if ($self->product_obj && $self->product_obj->name eq $name)
|
||||
|
@ -2161,7 +2217,18 @@ sub _set_product
|
|||
Bugzilla->user->can_enter_product($name, THROW_ERROR);
|
||||
# can_enter_product already does everything that check_product
|
||||
# would do for us, so we don't need to use it.
|
||||
my $product = new Bugzilla::Product({ name => $name });
|
||||
$product = new Bugzilla::Product({ name => $name });
|
||||
}
|
||||
else
|
||||
{
|
||||
$product = $name;
|
||||
# If we're updating the bug and they haven't changed the product, always allow it.
|
||||
if ($self->product_id == $product->id)
|
||||
{
|
||||
return undef;
|
||||
}
|
||||
Bugzilla->user->can_enter_product($product, THROW_ERROR);
|
||||
}
|
||||
if (($self->product_id || 0) != $product->id)
|
||||
{
|
||||
$self->{product_id} = $product->id;
|
||||
|
@ -3490,6 +3557,12 @@ sub ValidateTime
|
|||
return $time;
|
||||
}
|
||||
|
||||
sub get_activity
|
||||
{
|
||||
my $self = shift;
|
||||
return GetBugActivity($self->id, @_);
|
||||
}
|
||||
|
||||
# Get the activity of a bug, starting from $starttime (if given).
|
||||
# This routine assumes Bugzilla::Bug->check has been previously called.
|
||||
sub GetBugActivity
|
||||
|
@ -3623,7 +3696,9 @@ sub GetBugActivity
|
|||
{
|
||||
my $change = $operations[$i]{changes}[$j];
|
||||
my $field = Bugzilla->get_field($change->{fieldname});
|
||||
if ($change->{fieldname} eq 'longdesc' || $field->{type} eq FIELD_TYPE_TEXTAREA)
|
||||
if ($change->{fieldname} eq 'longdesc' ||
|
||||
$field->{type} == FIELD_TYPE_TEXTAREA ||
|
||||
$field->{type} == FIELD_TYPE_EAV_TEXTAREA)
|
||||
{
|
||||
my $diff = Bugzilla::Diff->new($change->{removed}, $change->{added})->get_table;
|
||||
if (!@$diff)
|
||||
|
@ -4283,7 +4358,14 @@ sub get_value
|
|||
elsif ($field->type == FIELD_TYPE_BUG_ID_REV)
|
||||
{
|
||||
$self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
|
||||
"SELECT bug_id FROM bugs WHERE ".$field->value_field->name." = ".$self->id
|
||||
"SELECT bug_id FROM bugs WHERE ".$field->value_field->name." = ?", undef, $self->id
|
||||
);
|
||||
return $self->{$attr};
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_EAV_TEXTAREA)
|
||||
{
|
||||
($self->{$attr}) = Bugzilla->dbh->selectrow_array(
|
||||
"SELECT value FROM bug_$attr WHERE bug_id=?", undef, $self->id
|
||||
);
|
||||
return $self->{$attr};
|
||||
}
|
||||
|
@ -4309,7 +4391,7 @@ sub fields
|
|||
map { $_->name } Bugzilla->get_fields({
|
||||
obsolete => 0,
|
||||
custom => 1,
|
||||
type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_ID_REV ],
|
||||
type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_EAV_TEXTAREA, FIELD_TYPE_BUG_ID_REV ],
|
||||
}),
|
||||
);
|
||||
Bugzilla::Hook::process('bug_fields', { fields => \@fields });
|
||||
|
@ -4334,7 +4416,7 @@ sub _validate_attribute
|
|||
# every DB column may be returned via an autoloaded accessor
|
||||
(map { $_ => 1 } Bugzilla::Bug->DB_COLUMNS),
|
||||
# multiselect, bug_id_rev fields
|
||||
(map { $_->name => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_ID_REV ] })),
|
||||
(map { $_->name => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_EAV_TEXTAREA, FIELD_TYPE_BUG_ID_REV ] })),
|
||||
# get_object accessors
|
||||
(map { $_->name.'_obj' => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT ] })),
|
||||
};
|
||||
|
|
|
@ -270,7 +270,7 @@ sub param
|
|||
# When we are just requesting the value of a parameter...
|
||||
if (scalar(@_) == 1)
|
||||
{
|
||||
my @result = $self->SUPER::param(@_);
|
||||
my @result = $self->SUPER::multi_param(@_);
|
||||
|
||||
# Also look at the URL parameters, after we look at the POST
|
||||
# parameters. This is to allow things like login-form submissions
|
||||
|
|
|
@ -43,6 +43,7 @@ use constant DB_COLUMNS => (
|
|||
'sql_code', # SQL code for query is cached here
|
||||
'except_fields', # "Exception" fields - see CF_DENY above.
|
||||
'triggers', # Triggers (bug changes) (requires CF_FREEZE & !CF_FATAL)
|
||||
'bypass_group_id', # Group members of which may bypass this check
|
||||
);
|
||||
use constant NAME_FIELD => 'message';
|
||||
use constant ID_FIELD => 'id';
|
||||
|
@ -53,16 +54,11 @@ use constant REQUIRED_CREATE_FIELDS => qw(query_id message);
|
|||
use constant VALIDATORS => {
|
||||
query_id => \&_check_query_id,
|
||||
flags => \&_check_flags,
|
||||
bypass_group_id => \&_check_bypass_group_id,
|
||||
user_id => \&_check_user_id,
|
||||
};
|
||||
|
||||
use constant UPDATE_COLUMNS => (
|
||||
'query_id',
|
||||
'flags',
|
||||
'message',
|
||||
'sql_code',
|
||||
'except_fields',
|
||||
'triggers',
|
||||
);
|
||||
use constant UPDATE_COLUMNS => (grep { $_ ne 'id' && $_ ne 'user_id' } DB_COLUMNS);
|
||||
|
||||
# The check works by executing this SQL query with added bugs.bug_id=? condition.
|
||||
# Rebuild and save SQL code in the DB, from under the superuser
|
||||
|
@ -80,6 +76,7 @@ sub refresh_sql
|
|||
params => http_decode_query($query->query),
|
||||
fields => [ 'bug_id' ],
|
||||
user => $query->user,
|
||||
ignore_permissions => 1,
|
||||
);
|
||||
my $terms = Bugzilla::Search::simplify_expression([
|
||||
'AND_MANY', { term => 'bugs.bug_id=?' },
|
||||
|
@ -125,14 +122,9 @@ sub update
|
|||
sub _check_query_id
|
||||
{
|
||||
my ($invocant, $value, $field) = @_;
|
||||
my $q = Bugzilla::Search::Saved->check({ id => $value });
|
||||
# This code allows to create predicates using searches shared by other users,
|
||||
# but the UI doesn't allow it (yet?).
|
||||
if ($q->user->id != Bugzilla->user->id &&
|
||||
(!$q->shared_with_group || !Bugzilla->user->in_group($q->shared_with_group)))
|
||||
{
|
||||
ThrowUserError('query_access_denied', { query => $q });
|
||||
}
|
||||
my $q = Bugzilla::Search::Saved->check({ id => $value });
|
||||
# Check if a named query is not a search query, but just an HTTP url
|
||||
if ($q->query =~ /^[a-z][a-z0-9]*:/iso)
|
||||
{
|
||||
|
@ -141,6 +133,18 @@ sub _check_query_id
|
|||
return $q->id;
|
||||
}
|
||||
|
||||
sub _check_user_id
|
||||
{
|
||||
my ($invocant, $value, $field) = @_;
|
||||
return Bugzilla->user->id;
|
||||
}
|
||||
|
||||
sub _check_bypass_group_id
|
||||
{
|
||||
my ($invocant, $value, $field) = @_;
|
||||
return $value ? Bugzilla::Group->check({ id => $value })->id : undef;
|
||||
}
|
||||
|
||||
sub _check_flags
|
||||
{
|
||||
my ($invocant, $value, $field) = @_;
|
||||
|
@ -153,6 +157,7 @@ sub query_id { $_[0]->{query_id} }
|
|||
sub user_id { $_[0]->{user_id} }
|
||||
sub message { $_[0]->{message} }
|
||||
sub sql_code { $_[0]->{sql_code} }
|
||||
sub bypass_group_id { $_[0]->{bypass_group_id} }
|
||||
sub flags { $_[0]->{flags} }
|
||||
|
||||
# Specific flags from the bitfield
|
||||
|
@ -214,8 +219,8 @@ sub user
|
|||
return $self->{user};
|
||||
}
|
||||
|
||||
sub set_query_id { $_[0]->set('query_id', Bugzilla::Search::Saved->check({ id => $_[1] })->id) }
|
||||
sub set_user_id { $_[0]->set('user_id', Bugzilla::User->check({ userid => $_[1] })->id) }
|
||||
sub set_query_id { $_[0]->set('query_id', $_[1]) }
|
||||
sub set_user_id { $_[0]->set('user_id', $_[1]) }
|
||||
sub set_flags { $_[0]->set('flags', $_[1]) }
|
||||
sub set_message { $_[0]->set('message', $_[1]) }
|
||||
sub set_sql_code { $_[0]->set('sql_code', $_[1]) }
|
||||
|
|
|
@ -53,11 +53,13 @@ sub check
|
|||
my $sql = [];
|
||||
my @bind;
|
||||
my ($s, $i);
|
||||
for (values %$all)
|
||||
for my $checker (values %$all)
|
||||
{
|
||||
if (($_->flags & $mask) == $flags)
|
||||
if (($checker->flags & $mask) == $flags &&
|
||||
# Do not run checkers which may be bypassed by user based on his permissions
|
||||
(!$checker->bypass_group_id || !Bugzilla->user->in_group_id($checker->bypass_group_id)))
|
||||
{
|
||||
$s = $_->sql_code;
|
||||
$s = $checker->sql_code;
|
||||
push @$sql, $s;
|
||||
push @bind, $bug_id;
|
||||
}
|
||||
|
@ -180,24 +182,24 @@ sub filter_failed_checkers
|
|||
my ($checkers, $changes, $bug) = @_;
|
||||
# Filter failed checkers by changes
|
||||
my @rc;
|
||||
for (@$checkers)
|
||||
for my $checker (@$checkers)
|
||||
{
|
||||
if ($_->triggers)
|
||||
if ($checker->triggers)
|
||||
{
|
||||
# Skip triggers
|
||||
push @rc, $_;
|
||||
push @rc, $checker;
|
||||
next;
|
||||
}
|
||||
my $e = $_->except_fields;
|
||||
my $e = $checker->except_fields;
|
||||
my $ok = 1;
|
||||
if ($_->deny_all)
|
||||
if ($checker->deny_all)
|
||||
{
|
||||
# Allow only changes of except_fields to except values
|
||||
for (keys %$changes)
|
||||
for my $field (keys %$changes)
|
||||
{
|
||||
# If the field is not listed in except_fields, OR
|
||||
# if there is a specific value in except_fields and our one is not equal
|
||||
if (!exists $e->{$_} || (defined $e->{$_} && $changes->{$_}->[1] ne $e->{$_}))
|
||||
if (!exists $e->{$field} || (defined $e->{$field} && !grep { $_ eq $changes->{$field}->[1] } list($e->{$field})))
|
||||
{
|
||||
$ok = 0;
|
||||
last;
|
||||
|
@ -207,20 +209,20 @@ sub filter_failed_checkers
|
|||
else
|
||||
{
|
||||
# Forbid changes of except_fields to except values
|
||||
for (keys %$e)
|
||||
for my $field (keys %$e)
|
||||
{
|
||||
# work_time_date is a special pseudo-field meaning addition of backdated worktime
|
||||
# the value of this pseudo-field is the date before which it is forbidden to fix worktime
|
||||
# for example except_fields={work_time_date=2010-09-01} means forbid fixing worktime
|
||||
# for dates before 2010-09-01
|
||||
if ($_ eq 'work_time_date')
|
||||
if ($field eq 'work_time_date')
|
||||
{
|
||||
my $today_date = strftime('%Y-%m-%d', localtime);
|
||||
my $min_backdate = $e->{$_} || $today_date;
|
||||
my $min_backdate = $e->{$field} || $today_date;
|
||||
my $min_comment_date;
|
||||
foreach (@{$bug->{added_comments} || []})
|
||||
foreach my $comment (@{$bug->{added_comments} || []})
|
||||
{
|
||||
my $cd = $_->{bug_when} || $today_date;
|
||||
my $cd = $comment->{bug_when} || $today_date;
|
||||
if (!$min_comment_date || $cd lt $min_comment_date)
|
||||
{
|
||||
$min_comment_date = $cd;
|
||||
|
@ -232,14 +234,14 @@ sub filter_failed_checkers
|
|||
last;
|
||||
}
|
||||
}
|
||||
elsif ($changes->{$_} && (!defined $e->{$_} || $changes->{$_}->[1] eq $e->{$_}))
|
||||
elsif ($changes->{$field} && (!defined $e->{$field} || grep { $_ eq $changes->{$field}->[1] } list($e->{$field})))
|
||||
{
|
||||
$ok = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
push @rc, $_ unless $ok;
|
||||
push @rc, $checker unless $ok;
|
||||
}
|
||||
@$checkers = @rc;
|
||||
}
|
||||
|
@ -380,7 +382,6 @@ sub install_before_final_checks
|
|||
{
|
||||
my ($args) = @_;
|
||||
print "Refreshing Checkers SQL...\n" if !$args->{silent};
|
||||
Bugzilla->request_cache->{user} = Bugzilla::User->super_user;
|
||||
for (Bugzilla::Checker->get_all)
|
||||
{
|
||||
eval { $_->update };
|
||||
|
|
|
@ -90,6 +90,12 @@ sub new
|
|||
|
||||
my $product;
|
||||
if (ref $param)
|
||||
{
|
||||
if ($param->{id})
|
||||
{
|
||||
$param = { condition => 'id = ?', values => [ $param->{id} ] };
|
||||
}
|
||||
else
|
||||
{
|
||||
$product = $param->{product};
|
||||
my $name = $param->{name};
|
||||
|
@ -107,11 +113,11 @@ sub new
|
|||
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(@_);
|
||||
|
@ -507,6 +513,7 @@ sub description { return $_[0]->{description}; }
|
|||
sub wiki_url { return $_[0]->{wiki_url}; }
|
||||
sub product_id { return $_[0]->{product_id}; }
|
||||
sub is_active { return $_[0]->{isactive}; }
|
||||
sub full_name { return $_[0]->product->name.'/'.$_[0]->name; }
|
||||
|
||||
###############################
|
||||
#### Subroutines ####
|
||||
|
|
|
@ -72,14 +72,14 @@ sub get_param_list
|
|||
{
|
||||
name => 'webdotbase',
|
||||
type => 't',
|
||||
default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%',
|
||||
default => '/usr/bin/dot',
|
||||
checker => \&check_webdotbase
|
||||
},
|
||||
|
||||
{
|
||||
name => 'webtwopibase',
|
||||
type => 't',
|
||||
default => '',
|
||||
default => '/usr/bin/twopi',
|
||||
checker => \&check_webdotbase
|
||||
},
|
||||
|
||||
|
|
|
@ -109,6 +109,12 @@ sub get_param_list
|
|||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'forbid_open_products',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
);
|
||||
return @param_list;
|
||||
}
|
||||
|
|
|
@ -26,14 +26,8 @@ sub get_param_list
|
|||
},
|
||||
|
||||
{
|
||||
name => 'viewvc_url',
|
||||
type => 't',
|
||||
default => '',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'git_url',
|
||||
type => 't',
|
||||
name => 'look_in_urls',
|
||||
type => 'l',
|
||||
default => '',
|
||||
},
|
||||
|
||||
|
|
|
@ -103,6 +103,12 @@ sub get_param_list
|
|||
default => 1
|
||||
},
|
||||
|
||||
{
|
||||
name => 'enable_inmail_cgi',
|
||||
type => 'b',
|
||||
default => 0
|
||||
},
|
||||
|
||||
{
|
||||
name => 'smtpserver',
|
||||
type => 't',
|
||||
|
|
|
@ -77,6 +77,14 @@ sub get_param_list
|
|||
type => 't',
|
||||
default => 'en',
|
||||
},
|
||||
|
||||
{
|
||||
name => 'sphinx_max_matches',
|
||||
type => 't',
|
||||
default => 1000,
|
||||
checker => sub { $_[0] =~ /^[1-9]\d*$/so ? "" : "must be a positive integer value" },
|
||||
},
|
||||
|
||||
);
|
||||
return @param_list;
|
||||
}
|
||||
|
|
|
@ -129,6 +129,8 @@ use Cwd qw(abs_path);
|
|||
FIELD_TYPE_NUMERIC
|
||||
FIELD_TYPE_EXTURL
|
||||
FIELD_TYPE_BUG_ID_REV
|
||||
FIELD_TYPE_EAV_TEXTAREA
|
||||
FIELD_TYPE__MAX
|
||||
|
||||
FLAG_VISIBLE
|
||||
FLAG_NULLABLE
|
||||
|
@ -194,7 +196,7 @@ use Cwd qw(abs_path);
|
|||
# CONSTANTS
|
||||
#
|
||||
# Bugzilla version
|
||||
use constant BUGZILLA_VERSION => "2014.10-beta";
|
||||
use constant BUGZILLA_VERSION => "2016.09";
|
||||
|
||||
# These are unique values that are unlikely to match a string or a number,
|
||||
# to be used in criteria for match() functions and other things. They start
|
||||
|
@ -392,6 +394,8 @@ use constant FIELD_TYPE_KEYWORDS => 8;
|
|||
use constant FIELD_TYPE_NUMERIC => 30;
|
||||
use constant FIELD_TYPE_EXTURL => 31;
|
||||
use constant FIELD_TYPE_BUG_ID_REV => 32;
|
||||
use constant FIELD_TYPE_EAV_TEXTAREA => 33;
|
||||
use constant FIELD_TYPE__MAX => 33;
|
||||
|
||||
use constant FLAG_VISIBLE => 0;
|
||||
use constant FLAG_NULLABLE => -1;
|
||||
|
|
|
@ -138,7 +138,7 @@ sub connect_sphinx
|
|||
mysql_enable_utf8 => 1,
|
||||
# Needs to be explicitly specified for command-line processes.
|
||||
mysql_auto_reconnect => 1,
|
||||
raise_error => 0,
|
||||
raise_error => 1,
|
||||
});
|
||||
|
||||
$sphinx->do("SET NAMES utf8") if $sphinx;
|
||||
|
@ -816,12 +816,18 @@ sub _bz_add_field_table {
|
|||
$self->bz_add_table($name);
|
||||
}
|
||||
|
||||
sub bz_add_field_tables {
|
||||
sub bz_add_field_tables
|
||||
{
|
||||
my ($self, $field) = @_;
|
||||
|
||||
$self->_bz_add_field_table($field->name,
|
||||
$self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type);
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT ||
|
||||
$field->type == FIELD_TYPE_SINGLE_SELECT)
|
||||
{
|
||||
$self->_bz_add_field_table(
|
||||
$field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type
|
||||
);
|
||||
}
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT)
|
||||
{
|
||||
my $ms_table = "bug_" . $field->name;
|
||||
$self->_bz_add_field_table($ms_table,
|
||||
$self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
|
||||
|
@ -832,15 +838,28 @@ sub bz_add_field_tables {
|
|||
$self->bz_add_fk($ms_table, 'value_id', {TABLE => $field->name,
|
||||
COLUMN => 'id'});
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_EAV_TEXTAREA)
|
||||
{
|
||||
my $ms_table = "bug_" . $field->name;
|
||||
$self->_bz_add_field_table($ms_table, $self->_bz_schema->EAV_TEXTAREA_VALUE_TABLE);
|
||||
$self->bz_add_fk($ms_table, 'bug_id', {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'});
|
||||
}
|
||||
}
|
||||
|
||||
sub bz_drop_field_tables {
|
||||
sub bz_drop_field_tables
|
||||
{
|
||||
my ($self, $field) = @_;
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT) {
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT ||
|
||||
$field->type == FIELD_TYPE_EAV_TEXTAREA)
|
||||
{
|
||||
$self->bz_drop_table('bug_' . $field->name);
|
||||
}
|
||||
if ($field->type == FIELD_TYPE_MULTI_SELECT ||
|
||||
$field->type == FIELD_TYPE_SINGLE_SELECT)
|
||||
{
|
||||
$self->bz_drop_table($field->name);
|
||||
}
|
||||
}
|
||||
|
||||
sub bz_drop_column
|
||||
{
|
||||
|
|
|
@ -825,6 +825,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
sql_code => {TYPE => 'LONGTEXT'},
|
||||
except_fields => {TYPE => 'LONGBLOB'},
|
||||
triggers => {TYPE => 'LONGBLOB'},
|
||||
bypass_group_id=> {TYPE => 'INT4', REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'}},
|
||||
],
|
||||
INDEXES => [
|
||||
checkers_query_id_idx => { FIELDS => ['query_id'] },
|
||||
|
@ -1131,6 +1132,7 @@ use constant ABSTRACT_SCHEMA => {
|
|||
sortkey => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => '0'},
|
||||
onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
|
||||
title => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
|
||||
isreport => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
|
||||
],
|
||||
INDEXES => [
|
||||
whine_queries_eventid_idx => ['eventid'],
|
||||
|
@ -1324,6 +1326,15 @@ use constant MULTI_SELECT_VALUE_TABLE => {
|
|||
bug_id_idx => {FIELDS => [qw(bug_id value_id)], TYPE => 'UNIQUE'},
|
||||
],
|
||||
};
|
||||
use constant EAV_TEXTAREA_VALUE_TABLE => {
|
||||
FIELDS => [
|
||||
bug_id => {TYPE => 'INT4', NOTNULL => 1},
|
||||
value => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"},
|
||||
],
|
||||
INDEXES => [
|
||||
bug_id_idx => {FIELDS => [qw(bug_id)], TYPE => 'UNIQUE'},
|
||||
],
|
||||
};
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ use Bugzilla::Mailer;
|
|||
use Date::Format;
|
||||
use JSON;
|
||||
use Data::Dumper;
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
use overload '""' => sub { $_[0]->{message} };
|
||||
|
||||
|
@ -34,7 +35,7 @@ sub _in_eval
|
|||
my $in = -$IN_EVAL;
|
||||
for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++)
|
||||
{
|
||||
$in--, last if $sub =~ /^Bugzilla::HTTPServerSimple/;
|
||||
last if $sub =~ /^Bugzilla::HTTPServerSimple/;
|
||||
last if $sub =~ /^ModPerl/;
|
||||
if ($sub =~ /^\(eval\)/)
|
||||
{
|
||||
|
@ -66,12 +67,33 @@ sub _error_message
|
|||
{
|
||||
$cgivars->{$_} = $cgi->uploadInfo($cgivars->{$_}) if $cgi->upload($_);
|
||||
}
|
||||
$mesg .= Data::Dumper->Dump([$vars, $cgivars, { %ENV }], ['error_vars', 'cgi_params', 'env']);
|
||||
$mesg .= Data::Dumper->Dump([remove_objects($vars), $cgivars, { %ENV }], ['error_vars', 'cgi_params', 'env']);
|
||||
# ugly workaround for Data::Dumper's \x{425} unicode characters
|
||||
$mesg =~ s/((?:\\x\{(?:[\dA-Z]+)\})+)/eval("\"$1\"")/egiso;
|
||||
return $mesg;
|
||||
}
|
||||
|
||||
sub remove_objects
|
||||
{
|
||||
my ($v) = @_;
|
||||
if (blessed $v)
|
||||
{
|
||||
return "$v";
|
||||
}
|
||||
elsif (ref($v) eq 'HASH')
|
||||
{
|
||||
return { map { ($_ => remove_objects($v->{$_})) } keys %$v };
|
||||
}
|
||||
elsif (ref($v) eq 'ARRAY')
|
||||
{
|
||||
return [ map { remove_objects($_) } @$v ];
|
||||
}
|
||||
else
|
||||
{
|
||||
return $v;
|
||||
}
|
||||
}
|
||||
|
||||
sub throw
|
||||
{
|
||||
my $self = shift;
|
||||
|
|
|
@ -306,7 +306,7 @@ sub _check_type
|
|||
# higher field type is added.
|
||||
if (!detaint_natural($type) || $type <= FIELD_TYPE_UNKNOWN ||
|
||||
$type > FIELD_TYPE_KEYWORDS && $type < FIELD_TYPE_NUMERIC ||
|
||||
$type > FIELD_TYPE_BUG_ID_REV)
|
||||
$type > FIELD_TYPE__MAX)
|
||||
{
|
||||
ThrowCodeError('invalid_customfield_type', { type => $saved_type });
|
||||
}
|
||||
|
@ -551,7 +551,8 @@ sub can_tweak
|
|||
return 1;
|
||||
}
|
||||
|
||||
# Return valid values for this field, arrayref of Bugzilla::Field::Choice objects.
|
||||
# Return valid values for this field, arrayref of Bugzilla::Field::Choice objects,
|
||||
# filtered by the current user's permissions.
|
||||
# Includes disabled values is $include_disabled == true
|
||||
sub legal_values
|
||||
{
|
||||
|
@ -892,11 +893,8 @@ sub remove_from_db
|
|||
$dbh->bz_drop_column('bugs', $name);
|
||||
}
|
||||
|
||||
if ($self->is_select)
|
||||
{
|
||||
# Delete the table that holds the legal values for this field.
|
||||
$dbh->bz_drop_field_tables($self);
|
||||
}
|
||||
|
||||
$self->set_visibility_values(undef);
|
||||
$self->set_null_visibility_values(undef);
|
||||
|
@ -943,24 +941,24 @@ sub touch
|
|||
sub set_visibility_values
|
||||
{
|
||||
my $self = shift;
|
||||
my ($value_ids) = @_;
|
||||
$self->update_visibility_values(FLAG_VISIBLE, $value_ids);
|
||||
my ($value_ids, $skip_invisible) = @_;
|
||||
$self->update_visibility_values(FLAG_VISIBLE, $value_ids, $skip_invisible);
|
||||
return $value_ids && @$value_ids;
|
||||
}
|
||||
|
||||
sub set_null_visibility_values
|
||||
{
|
||||
my $self = shift;
|
||||
my ($value_ids) = @_;
|
||||
$self->update_visibility_values(FLAG_NULLABLE, $value_ids);
|
||||
my ($value_ids, $skip_invisible) = @_;
|
||||
$self->update_visibility_values(FLAG_NULLABLE, $value_ids, $skip_invisible);
|
||||
return $value_ids && @$value_ids;
|
||||
}
|
||||
|
||||
sub set_clone_visibility_values
|
||||
{
|
||||
my $self = shift;
|
||||
my ($value_ids) = @_;
|
||||
$self->update_visibility_values(FLAG_CLONED, $value_ids);
|
||||
my ($value_ids, $skip_invisible) = @_;
|
||||
$self->update_visibility_values(FLAG_CLONED, $value_ids, $skip_invisible);
|
||||
return $value_ids && @$value_ids;
|
||||
}
|
||||
|
||||
|
@ -992,7 +990,7 @@ sub clear_default_values
|
|||
sub update_visibility_values
|
||||
{
|
||||
my $self = shift;
|
||||
my ($controlled_value_id, $visibility_value_ids) = @_;
|
||||
my ($controlled_value_id, $visibility_value_ids, $skip_invisible) = @_;
|
||||
$visibility_value_ids ||= [];
|
||||
my $vis_field = $self->flag_field($controlled_value_id);
|
||||
if (!$vis_field)
|
||||
|
@ -1014,6 +1012,16 @@ sub update_visibility_values
|
|||
$h = $h->{null}->{$self->id} if $controlled_value_id == FLAG_NULLABLE;
|
||||
$h = $h->{clone}->{$self->id} if $controlled_value_id == FLAG_CLONED;
|
||||
$h = $h ? { %$h } : {};
|
||||
if ($skip_invisible)
|
||||
{
|
||||
# Do not affect visibility values the user can't see
|
||||
# so he can't damage other user's visibility values for the same field value
|
||||
my $allowed = { map { $_->id => 1 } @{$vis_field->legal_values} };
|
||||
for (keys %$h)
|
||||
{
|
||||
delete $h->{$_} if !$allowed->{$_};
|
||||
}
|
||||
}
|
||||
my $add = [];
|
||||
for (@$visibility_value_ids)
|
||||
{
|
||||
|
@ -1267,13 +1275,8 @@ sub create
|
|||
# Create the database column that stores the data for this field.
|
||||
$dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
|
||||
}
|
||||
|
||||
if ($obj->is_select)
|
||||
{
|
||||
# Create the table that holds the legal values for this field.
|
||||
$dbh->bz_add_field_tables($obj);
|
||||
}
|
||||
|
||||
# Add foreign keys
|
||||
if ($type == FIELD_TYPE_SINGLE_SELECT)
|
||||
{
|
||||
|
|
|
@ -314,6 +314,7 @@ sub get_all_names
|
|||
|
||||
sub is_active { return $_[0]->{isactive}; }
|
||||
sub sortkey { return $_[0]->{sortkey}; }
|
||||
sub full_name { return $_[0]->name; }
|
||||
|
||||
# FIXME Never use bug_count() on a copy from legal_values, as the result will be cached...
|
||||
sub bug_count
|
||||
|
@ -413,8 +414,8 @@ sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
|
|||
sub set_visibility_values
|
||||
{
|
||||
my $self = shift;
|
||||
my ($value_ids) = @_;
|
||||
$self->field->update_visibility_values($self->id, $value_ids);
|
||||
my ($value_ids, $skip_invisible) = @_;
|
||||
$self->field->update_visibility_values($self->id, $value_ids, $skip_invisible);
|
||||
delete $self->{visibility_values};
|
||||
return $value_ids;
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ sub AddWorktime
|
|||
my $remaining_time = $bug->remaining_time;
|
||||
my $newrtime = $remaining_time - $sum;
|
||||
$newrtime = 0 if $newrtime < 0;
|
||||
$bug->remaining_time($newrtime) if $newrtime != $remaining_time;
|
||||
$bug->set('remaining_time', $newrtime) if $newrtime != $remaining_time;
|
||||
|
||||
$bug->update();
|
||||
|
||||
|
|
|
@ -150,6 +150,7 @@ sub status { return $_[0]->{status}; }
|
|||
sub setter_id { return $_[0]->{setter_id}; }
|
||||
sub requestee_id { return $_[0]->{requestee_id}; }
|
||||
sub creation_date { return $_[0]->{creation_date}; }
|
||||
sub modification_date { return $_[0]->{modification_date}; }
|
||||
|
||||
###############################
|
||||
#### Methods ####
|
||||
|
|
|
@ -373,6 +373,123 @@ sub remove_from_db
|
|||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
sub add_users
|
||||
{
|
||||
my $self = shift;
|
||||
my ($users, $isbless) = @_;
|
||||
return if !@$users;
|
||||
add_user_groups([ map { { group => $self, user => $_ } } @$users ], $isbless);
|
||||
}
|
||||
|
||||
sub remove_users
|
||||
{
|
||||
my $self = shift;
|
||||
my ($users, $isbless) = @_;
|
||||
return if !@$users;
|
||||
remove_user_groups([ map { { group => $self, user => $_ } } @$users ], $isbless);
|
||||
}
|
||||
|
||||
# Add members or blessers to this group
|
||||
# Bugzilla::Group::add_user_groups([ { user => Bugzilla::User or int id, group => Bugzilla::Group }, ... ], $isbless = 0 or 1)
|
||||
sub add_user_groups
|
||||
{
|
||||
shift if $_[0] eq __PACKAGE__;
|
||||
my ($rows, $isbless) = @_;
|
||||
return if !@$rows;
|
||||
# Filter duplicates
|
||||
my $g = {};
|
||||
for my $row (@$rows)
|
||||
{
|
||||
$g->{int(ref $row->{user} ? $row->{user}->id : $row->{user})}->{int($row->{group}->id)} = $row;
|
||||
}
|
||||
$isbless = $isbless ? 1 : 0;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
# Filter already existing members
|
||||
for my $row (@{ $dbh->selectall_arrayref(
|
||||
"SELECT user_id, group_id FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
|
||||
join(', ', map {
|
||||
my $uid = $_;
|
||||
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } keys %{$g->{$uid}};
|
||||
} keys %$g).") FOR UPDATE", undef
|
||||
) || [] })
|
||||
{
|
||||
delete $g->{$row->[0]}->{$row->[1]};
|
||||
delete $g->{$row->[0]} if !%{$g->{$row->[0]}};
|
||||
}
|
||||
return if !%$g;
|
||||
# Apply update
|
||||
$dbh->do(
|
||||
"INSERT INTO user_group_map (user_id, group_id, grant_type, isbless) VALUES ".
|
||||
join(', ', map {
|
||||
my $uid = $_;
|
||||
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } keys %{$g->{$uid}};
|
||||
} keys %$g)
|
||||
);
|
||||
# Record profiles_activity entries
|
||||
my $cur_userid = Bugzilla->user->id;
|
||||
my $group_fldid = Bugzilla->get_field('bug_group')->id;
|
||||
if (!$isbless)
|
||||
{
|
||||
# FIXME: should create profiles_activity entries for blesser changes.
|
||||
$dbh->do(
|
||||
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ".
|
||||
join(', ', map {
|
||||
my $uid = $_;
|
||||
join(', ', map { "($uid, $cur_userid, NOW(), $group_fldid, '', ".$dbh->quote($_->{group}->name).")" } values %{$g->{$uid}})
|
||||
} keys %$g)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# Remove members or blessers from this group - arguments same as in add_user_groups()
|
||||
sub remove_user_groups
|
||||
{
|
||||
shift if $_[0] eq __PACKAGE__;
|
||||
my ($rows, $isbless) = @_;
|
||||
return if !@$rows;
|
||||
# Filter duplicates
|
||||
$isbless = $isbless ? 1 : 0;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
# Remember group objects
|
||||
my $g = { map { $_->{group}->id => $_->{group} } @$rows };
|
||||
# Filter already deleted members
|
||||
my $del = {};
|
||||
for my $row (@{ $dbh->selectall_arrayref(
|
||||
"SELECT user_id, group_id FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
|
||||
join(', ', map {
|
||||
my $uid = int(ref $_->{user} ? $_->{user}->id : $_->{user});
|
||||
my $gid = int($_->{group}->id);
|
||||
"($uid, $gid, ".GRANT_DIRECT.", $isbless)";
|
||||
} @$rows).") FOR UPDATE", undef
|
||||
) || [] })
|
||||
{
|
||||
push @{$del->{$row->[0]}}, $row->[1];
|
||||
}
|
||||
return if !%$del;
|
||||
# Apply update
|
||||
$dbh->do(
|
||||
"DELETE FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
|
||||
join(', ', map {
|
||||
my $uid = $_;
|
||||
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } @{$del->{$uid}};
|
||||
} keys %$del).")"
|
||||
);
|
||||
# Record profiles_activity entries
|
||||
my $cur_userid = Bugzilla->user->id;
|
||||
my $group_fldid = Bugzilla->get_field('bug_group')->id;
|
||||
if (!$isbless)
|
||||
{
|
||||
# FIXME: should create profiles_activity entries for blesser changes.
|
||||
$dbh->do(
|
||||
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ".
|
||||
join(', ', map {
|
||||
my $uid = $_;
|
||||
map { "($uid, $cur_userid, NOW(), $group_fldid, ".$dbh->quote($g->{$_}->name).", '')" } @{$del->{$uid}};
|
||||
} keys %$del)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
# Add missing entries in bug_group_map for bugs created while
|
||||
# a mandatory group was disabled and which is now enabled again.
|
||||
sub _enforce_mandatory
|
||||
|
|
|
@ -0,0 +1,462 @@
|
|||
# Incoming mail handler for Bugzilla
|
||||
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
||||
# Contributor(s): Vitaliy Filippov <vitalif@mail.ru>, Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
|
||||
package Bugzilla::InMail;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Email::Address;
|
||||
use Email::Reply qw(reply);
|
||||
use Email::MIME;
|
||||
use Email::MIME::Attachment::Stripper;
|
||||
use HTML::Strip;
|
||||
use Getopt::Long qw(:config bundling);
|
||||
use Pod::Usage;
|
||||
use Encode;
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Attachment;
|
||||
use Bugzilla::Bug;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Mailer;
|
||||
use Bugzilla::Token;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Util;
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
||||
# This is the USENET standard line for beginning a signature block
|
||||
# in a message. RFC-compliant mailers use this.
|
||||
use constant SIGNATURE_DELIMITER => '-- ';
|
||||
|
||||
sub process_inmail
|
||||
{
|
||||
my ($mail_text) = @_;
|
||||
|
||||
my $input_email = Email::MIME->new($mail_text);
|
||||
|
||||
my $status = eval
|
||||
{
|
||||
my $mail_fields = parse_mail($input_email);
|
||||
if (!$mail_fields)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields });
|
||||
|
||||
my $attachments = delete $mail_fields->{attachments};
|
||||
select_user($mail_fields->{reporter}, $mail_fields->{_reporter_name});
|
||||
|
||||
my ($bug, $comment);
|
||||
if ($mail_fields->{bug_id})
|
||||
{
|
||||
$bug = Bugzilla::Bug::create_or_update($mail_fields);
|
||||
$comment = $bug->comments->[-1] if trim($mail_fields->{comment});
|
||||
}
|
||||
else
|
||||
{
|
||||
($bug, $comment) = post_bug($mail_fields);
|
||||
}
|
||||
|
||||
handle_attachments($bug, $attachments, $comment);
|
||||
|
||||
Bugzilla->send_mail;
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
if ($@)
|
||||
{
|
||||
# Report error to the sender of original message
|
||||
my $msg = $@;
|
||||
if (ref $msg eq 'Bugzilla::Error')
|
||||
{
|
||||
$msg = $msg->{message};
|
||||
}
|
||||
if ($input_email)
|
||||
{
|
||||
my $from = Bugzilla->params->{mailfrom};
|
||||
my $reply = reply(to => $input_email, from => $from, top_post => 1, body => "$msg\n");
|
||||
MessageToMTA($reply->as_string);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
sub select_user
|
||||
{
|
||||
my ($reporter, $reporter_name) = @_;
|
||||
|
||||
my $username = $reporter;
|
||||
# If emailsuffix is in use, we have to remove it from the email address.
|
||||
if (my $suffix = Bugzilla->params->{emailsuffix})
|
||||
{
|
||||
$username =~ s/\Q$suffix\E$//i;
|
||||
}
|
||||
|
||||
# First try to select user with name $username
|
||||
my $user = Bugzilla::User->new({ name => $username });
|
||||
|
||||
# Then try to find alias $username for some user
|
||||
unless ($user)
|
||||
{
|
||||
my $dbh = Bugzilla->dbh;
|
||||
($user) = $dbh->selectrow_array("SELECT userid FROM emailin_aliases WHERE address=?", undef, trim($reporter));
|
||||
$user = Bugzilla::User->new({ id => $user }) if $user;
|
||||
# Then check if autoregistration is enabled
|
||||
unless ($user)
|
||||
{
|
||||
unless (Bugzilla->params->{emailin_autoregister})
|
||||
{
|
||||
ThrowUserError('invalid_username', { name => $username });
|
||||
}
|
||||
# Then try to autoregister unknown user
|
||||
$user = Bugzilla::User->create({
|
||||
login_name => $username,
|
||||
realname => $reporter_name,
|
||||
cryptpassword => 'a3#',
|
||||
disabledtext => '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user->is_enabled)
|
||||
{
|
||||
ThrowUserError('account_disabled', { disabled_reason => $user->disabledtext });
|
||||
}
|
||||
|
||||
Bugzilla->set_user($user);
|
||||
}
|
||||
|
||||
sub parse_mail
|
||||
{
|
||||
my ($input_email) = @_;
|
||||
|
||||
my %fields;
|
||||
Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, fields => \%fields });
|
||||
# RFC 3834 - Recommendations for Automatic Responses to Electronic Mail
|
||||
# Automatic responses SHOULD NOT be issued in response to any
|
||||
# message which contains an Auto-Submitted header field (see below),
|
||||
# where that field has any value other than "no".
|
||||
# F*cking MS Exchange sometimes does not append Auto-Submitted header
|
||||
# to delivery status reports, so also check content-type.
|
||||
my $autosubmitted;
|
||||
if (lc($input_email->header('Auto-Submitted') || 'no') ne 'no' ||
|
||||
($input_email->header('X-Auto-Response-Suppress') || '') =~ /all/iso ||
|
||||
($input_email->header('Content-Type') || '') =~ /delivery-status/iso)
|
||||
{
|
||||
return undef;
|
||||
}
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Fetch field => value from emailin_fields table
|
||||
my ($toemail) = Email::Address->parse($input_email->header('To'));
|
||||
%fields = (%fields, map { @$_ } @{ $dbh->selectall_arrayref(
|
||||
"SELECT field, value FROM emailin_fields WHERE address=?",
|
||||
undef, $toemail) || [] });
|
||||
|
||||
my $summary = $input_email->header('Subject');
|
||||
if ($summary =~ /\[\s*Bug\s*(\d+)\s*\](.*)/i)
|
||||
{
|
||||
$fields{bug_id} = $1;
|
||||
$summary = trim($2);
|
||||
}
|
||||
$fields{_subject} = $summary;
|
||||
|
||||
# Add CC's from email Cc: header
|
||||
$fields{newcc} = $input_email->header('Cc');
|
||||
$fields{newcc} = $fields{newcc} && (join ', ', map { [ Email::Address->parse($_) ] -> [0] }
|
||||
split /\s*,\s*/, $fields{newcc}) || undef;
|
||||
|
||||
my ($body, $attachments) = get_body_and_attachments($input_email);
|
||||
if (@$attachments)
|
||||
{
|
||||
$fields{attachments} = $attachments;
|
||||
}
|
||||
|
||||
$body = remove_leading_blank_lines($body);
|
||||
|
||||
Bugzilla::Hook::process("emailin-filter_body", { body => \$body });
|
||||
|
||||
my @body_lines = split(/\r?\n/s, $body);
|
||||
my $fields_by_name = { map { (lc($_->description) => $_->name, lc($_->name) => $_->name) } Bugzilla->get_fields({ obsolete => 0 }) };
|
||||
|
||||
# If there are fields specified.
|
||||
if ($body =~ /^\s*@/s)
|
||||
{
|
||||
my $current_field;
|
||||
while (my $line = shift @body_lines)
|
||||
{
|
||||
# If the sig is starting, we want to keep this in the
|
||||
# @body_lines so that we don't keep the sig as part of the
|
||||
# comment down below.
|
||||
if ($line eq SIGNATURE_DELIMITER)
|
||||
{
|
||||
unshift(@body_lines, $line);
|
||||
last;
|
||||
}
|
||||
# Otherwise, we stop parsing fields on the first blank line.
|
||||
$line = trim($line);
|
||||
last if !$line;
|
||||
if ($line =~ /^\@\s*(.+?)\s*=\s*(.*)\s*/)
|
||||
{
|
||||
$current_field = $fields_by_name->{lc($1)} || lc($1);
|
||||
$fields{$current_field} = $2;
|
||||
}
|
||||
else
|
||||
{
|
||||
$fields{$current_field} .= " $line";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%fields = %{ Bugzilla::Bug::map_fields(\%fields) };
|
||||
|
||||
my ($reporter) = Email::Address->parse($input_email->header('From'));
|
||||
$fields{reporter} = $reporter->address;
|
||||
|
||||
{
|
||||
my $r;
|
||||
if ($r = $reporter->phrase)
|
||||
{
|
||||
$r .= ' ' . $reporter->comment if $reporter->comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
$r = $reporter->address;
|
||||
}
|
||||
$fields{_reporter_name} = $r;
|
||||
}
|
||||
|
||||
# The summary line only affects us if we're doing a post_bug.
|
||||
# We have to check it down here because there might have been
|
||||
# a bug_id specified in the body of the email.
|
||||
if (!$fields{bug_id} && !$fields{short_desc})
|
||||
{
|
||||
$fields{short_desc} = $summary;
|
||||
}
|
||||
|
||||
my $comment = '';
|
||||
# Get the description, except the signature.
|
||||
foreach my $line (@body_lines)
|
||||
{
|
||||
last if $line eq SIGNATURE_DELIMITER;
|
||||
$comment .= "$line\n";
|
||||
}
|
||||
$fields{comment} = $comment;
|
||||
|
||||
return \%fields;
|
||||
}
|
||||
|
||||
sub post_bug
|
||||
{
|
||||
my ($fields) = @_;
|
||||
my $bug;
|
||||
$Bugzilla::Error::IN_EVAL++;
|
||||
eval
|
||||
{
|
||||
my ($retval, $non_conclusive_fields) =
|
||||
Bugzilla::User::match_field({
|
||||
assigned_to => { type => 'single' },
|
||||
qa_contact => { type => 'single' },
|
||||
cc => { type => 'multi' }
|
||||
}, $fields, MATCH_SKIP_CONFIRM);
|
||||
if ($retval != USER_MATCH_SUCCESS)
|
||||
{
|
||||
ThrowUserError('user_match_too_many', { fields => $non_conclusive_fields });
|
||||
}
|
||||
$bug = Bugzilla::Bug::create_or_update($fields);
|
||||
};
|
||||
$Bugzilla::Error::IN_EVAL--;
|
||||
if (my $err = $@)
|
||||
{
|
||||
my $format = "\n\nIncoming mail format for entering bugs:\n\n\@field = value\n\@field = value\n...\n\n<Bug description...>\n";
|
||||
if (blessed $err && $err->{message})
|
||||
{
|
||||
$err->{message} .= $format;
|
||||
}
|
||||
else
|
||||
{
|
||||
$err .= $format;
|
||||
}
|
||||
die $err;
|
||||
}
|
||||
if ($bug)
|
||||
{
|
||||
return ($bug, $bug->comments->[0]);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub handle_attachments
|
||||
{
|
||||
my ($bug, $attachments, $comment) = @_;
|
||||
return if !$attachments;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_start_transaction();
|
||||
my ($update_comment, $update_bug);
|
||||
foreach my $attachment (@$attachments)
|
||||
{
|
||||
my $data = delete $attachment->{payload};
|
||||
$attachment->{content_type} ||= 'application/octet-stream';
|
||||
my $obj = Bugzilla::Attachment->create({
|
||||
bug => $bug,
|
||||
description => $attachment->{filename},
|
||||
filename => $attachment->{filename},
|
||||
mimetype => $attachment->{content_type},
|
||||
data => $data,
|
||||
});
|
||||
# If we added a comment, and our comment does not already have a type,
|
||||
# and this is our first attachment, then we make the comment an
|
||||
# "attachment created" comment.
|
||||
if ($comment and !$comment->type and !$update_comment)
|
||||
{
|
||||
$comment->set_type(CMT_ATTACHMENT_CREATED, $obj->id);
|
||||
$update_comment = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$bug->add_comment('', { type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id });
|
||||
$update_bug = 1;
|
||||
}
|
||||
}
|
||||
# We only update the comments and bugs at the end of the transaction,
|
||||
# because doing so modifies bugs_fulltext, which is a non-transactional
|
||||
# table.
|
||||
$bug->update() if $update_bug;
|
||||
$comment->update() if $update_comment;
|
||||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
######################
|
||||
# Helper Subroutines #
|
||||
######################
|
||||
|
||||
sub get_body_and_attachments
|
||||
{
|
||||
my ($email) = @_;
|
||||
|
||||
my $ct = $email->content_type || 'text/plain';
|
||||
|
||||
my $body;
|
||||
my $attachments = [];
|
||||
if ($ct =~ /^multipart\/(alternative|signed)/i)
|
||||
{
|
||||
$body = get_text_alternative($email);
|
||||
}
|
||||
else
|
||||
{
|
||||
my $stripper = new Email::MIME::Attachment::Stripper($email, force_filename => 1);
|
||||
my $message = $stripper->message;
|
||||
$body = get_text_alternative($message);
|
||||
$attachments = [$stripper->attachments];
|
||||
}
|
||||
$email->charset_set('utf8');
|
||||
$email->body_str_set($body);
|
||||
|
||||
return ($body, $attachments);
|
||||
}
|
||||
|
||||
sub rm_line_feeds
|
||||
{
|
||||
my ($t) = @_;
|
||||
$t =~ s/[\n\r]+/ /giso;
|
||||
return $t;
|
||||
}
|
||||
|
||||
sub get_text_alternative
|
||||
{
|
||||
my ($email) = @_;
|
||||
|
||||
my @parts = $email->parts;
|
||||
my $body;
|
||||
foreach my $part (@parts)
|
||||
{
|
||||
my $ct = $part->content_type || 'text/plain';
|
||||
my $charset = 'iso-8859-1';
|
||||
# The charset may be quoted.
|
||||
if ($ct =~ /charset="?([^;"]+)/)
|
||||
{
|
||||
$charset = $1;
|
||||
}
|
||||
if (!$ct || $ct =~ /^text\/plain/i)
|
||||
{
|
||||
$body = $part->body;
|
||||
}
|
||||
elsif ($ct =~ /^text\/html/i)
|
||||
{
|
||||
$body = $part->body;
|
||||
$body =~ s/<table[^<>]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso;
|
||||
$body =~ s/(<a[^<>]*>.*?<\/a\s*>)/rm_line_feeds($1)/gieso;
|
||||
Bugzilla::Hook::process("emailin-filter_html", { body => \$body });
|
||||
$body = HTML::Strip->new->parse($body);
|
||||
}
|
||||
if (defined $body)
|
||||
{
|
||||
if (Bugzilla->params->{utf8} && !utf8::is_utf8($body))
|
||||
{
|
||||
$body = Encode::decode($charset, $body);
|
||||
}
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined $body)
|
||||
{
|
||||
# Note that this only happens if the email does not contain any
|
||||
# text/plain parts. If the email has an empty text/plain part,
|
||||
# you're fine, and this message does NOT get thrown.
|
||||
ThrowUserError('email_no_text_plain');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
sub remove_leading_blank_lines
|
||||
{
|
||||
my ($text) = @_;
|
||||
$text =~ s/^(\s*\n)+//s;
|
||||
return $text;
|
||||
}
|
||||
|
||||
# Use UTF-8 in Email::Reply to correctly quote the body
|
||||
my $crlf = "\x0d\x0a";
|
||||
my $CRLF = $crlf;
|
||||
undef *Email::Reply::_quote_body;
|
||||
*Email::Reply::_quote_body = sub
|
||||
{
|
||||
my ($self, $part) = @_;
|
||||
return if length $self->{quoted};
|
||||
return map $self->_quote_body($_), $part->parts if $part->parts > 1;
|
||||
return if $part->content_type && $part->content_type !~ m[\btext/plain\b];
|
||||
|
||||
my $body = $part->body;
|
||||
Encode::_utf8_on($body);
|
||||
|
||||
$body = ($self->_strip_sig($body) || $body)
|
||||
if !$self->{keep_sig} && $body =~ /$crlf--\s*$crlf/o;
|
||||
|
||||
my ($end) = $body =~ /($crlf)/;
|
||||
$end ||= $CRLF;
|
||||
$body =~ s/[\r\n\s]+$//;
|
||||
$body = $self->_quote_orig_body($body);
|
||||
$body = "$self->{attrib}$end$body$end";
|
||||
|
||||
$self->{crlf} = $end;
|
||||
$self->{quoted} = $body;
|
||||
};
|
||||
|
||||
1;
|
||||
__END__
|
|
@ -94,6 +94,7 @@ use constant SYSTEM_GROUPS => (
|
|||
{ name => 'editusers' },
|
||||
{ name => 'creategroups' },
|
||||
{ name => 'editclassifications' },
|
||||
{ name => 'createproducts' },
|
||||
{ name => 'editcomponents' },
|
||||
{ name => 'editkeywords' },
|
||||
{ name => 'editbugs', userregexp => '.*' },
|
||||
|
@ -111,7 +112,7 @@ use constant SYSTEM_GROUPS => (
|
|||
{
|
||||
name => 'admin_index',
|
||||
include => [
|
||||
qw(tweakparams editusers editclassifications editcomponents creategroups
|
||||
qw(tweakparams editusers editclassifications createproducts editcomponents creategroups
|
||||
editfields editflagtypes editkeywords bz_canusewhines bz_editcheckers)
|
||||
],
|
||||
},
|
||||
|
@ -162,22 +163,33 @@ sub update_settings
|
|||
sub update_system_groups
|
||||
{
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
foreach my $definition (SYSTEM_GROUPS)
|
||||
{
|
||||
my $exists = new Bugzilla::Group({ name => $definition->{name} });
|
||||
my $grp = new Bugzilla::Group({ name => $definition->{name} });
|
||||
$definition->{isbuggroup} = 0;
|
||||
$definition->{description} = html_strip(Bugzilla->messages->{system_groups}->{$definition->{name}});
|
||||
my $include = delete $definition->{include};
|
||||
if (!$exists)
|
||||
if (!$grp)
|
||||
{
|
||||
Bugzilla::Group->create($definition);
|
||||
$grp = Bugzilla::Group->create($definition);
|
||||
}
|
||||
elsif ($definition->{name} ne 'admin_index')
|
||||
{
|
||||
# Always fix inclusions in admin_index
|
||||
$include = undef;
|
||||
}
|
||||
if ($include && @$include)
|
||||
{
|
||||
my $cur = { map { $_ => 1 } @{$dbh->selectcol_arrayref(
|
||||
'SELECT g.name FROM group_group_map gm, groups g'.
|
||||
' WHERE g.id=gm.member_id AND gm.grantor_id=? AND gm.grant_type=0', undef, $grp->id
|
||||
) || []} };
|
||||
$include = [ grep { !$cur->{$_} } @$include ];
|
||||
if (@$include)
|
||||
{
|
||||
$dbh->do(
|
||||
'INSERT INTO group_group_map (member_id, grantor_id, grant_type)'.
|
||||
' SELECT g.id, ai.id, 0 FROM groups ai, groups g WHERE ai.name=?'.
|
||||
' AND g.name IN (\''.join("','", @$include).'\')', undef, $definition->{name}
|
||||
' SELECT g.id, ?, 0 FROM groups g WHERE g.name IN (\''.join("','", @$include).'\')', undef, $grp->id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -864,6 +864,10 @@ WHERE description LIKE \'%[CC:%]%\'');
|
|||
|
||||
$dbh->bz_add_column('versions', 'sortkey');
|
||||
|
||||
$dbh->bz_add_column('checkers', 'bypass_group_id');
|
||||
|
||||
$dbh->bz_add_column('whine_queries', 'isreport');
|
||||
|
||||
_move_old_defaults($old_params);
|
||||
|
||||
################################################################
|
||||
|
@ -3519,7 +3523,7 @@ sub _populate_bugs_fulltext
|
|||
$datasize += length $_;
|
||||
}
|
||||
my $s = "($_, ".join(', ', map { $sph->$quote($_) } @{$rows->{$_}})."), ";
|
||||
if ($len + length $s >= $max_packet)
|
||||
if ($len > 0 && $len + length $s >= $max_packet)
|
||||
{
|
||||
last;
|
||||
}
|
||||
|
|
|
@ -66,9 +66,7 @@ sub REQUIRED_MODULES {
|
|||
{
|
||||
package => 'CGI.pm',
|
||||
module => 'CGI',
|
||||
# 3.51 fixes a security problem that affects Bugzilla.
|
||||
# (bug 591165)
|
||||
version => '3.51',
|
||||
version => '4.08',
|
||||
},
|
||||
{
|
||||
package => 'Digest-SHA',
|
||||
|
@ -132,11 +130,6 @@ sub REQUIRED_MODULES {
|
|||
module => 'Text::Wrap',
|
||||
version => '2013.0426',
|
||||
},
|
||||
{
|
||||
package => 'Text-TabularDisplay',
|
||||
module => 'Text::TabularDisplay',
|
||||
feature => 'Table formatting inside bug comments',
|
||||
},
|
||||
{
|
||||
package => 'LWP-MediaTypes',
|
||||
module => 'LWP::MediaTypes',
|
||||
|
|
|
@ -77,6 +77,7 @@ $Bugzilla::messages->{en} = {
|
|||
FIELD_TYPE_NUMERIC() => 'Numeric',
|
||||
FIELD_TYPE_EXTURL() => 'External URL',
|
||||
FIELD_TYPE_BUG_ID_REV() => $terms->{Bug}.' ID reverse',
|
||||
FIELD_TYPE_EAV_TEXTAREA() => 'Large Text Box (separate table)',
|
||||
},
|
||||
control_options => {
|
||||
CONTROLMAPNA() => 'NA',
|
||||
|
@ -85,6 +86,8 @@ $Bugzilla::messages->{en} = {
|
|||
CONTROLMAPMANDATORY() => 'Mandatory',
|
||||
},
|
||||
field_descs => {
|
||||
count => 'Number of '.$terms->{Bugs},
|
||||
times => 'Estimated/Actual/Remaining',
|
||||
alias => 'Alias',
|
||||
assigned_to => 'Assignee',
|
||||
blocked => 'Blocks',
|
||||
|
@ -219,6 +222,7 @@ $Bugzilla::messages->{en} = {
|
|||
tweakparams => 'Allows to <a href="editparams.cgi">change Parameters</a>.',
|
||||
editusers => 'Allows to <a href="editusers.cgi">edit or disable users</a> and include/exclude them from <b>all</b> groups.',
|
||||
creategroups => 'Allows to <a href="editgroups.cgi">create, destroy and edit groups</a>.',
|
||||
createproducts => 'Allows to <a href="editproducts.cgi">create new products</a>.',
|
||||
editclassifications => 'Allows to <a href="editclassifications.cgi">create, destroy and edit classifications</a>.',
|
||||
editcomponents => 'Allows to <a href="editproducts.cgi">create, destroy and edit all products, components, versions and milestones</a>.',
|
||||
editkeywords => 'Allows to <a href="editvalues.cgi?field=keywords">create, destroy and edit keywords</a>.',
|
||||
|
|
|
@ -151,6 +151,9 @@ sub MessageToMTA {
|
|||
username => Bugzilla->params->{"smtp_username"},
|
||||
password => Bugzilla->params->{"smtp_password"},
|
||||
helo => $hostname;
|
||||
if (Bugzilla->params->{smtp_debug}) {
|
||||
push @args, debug => 1;
|
||||
}
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('mailer_before_send',
|
||||
|
|
|
@ -121,6 +121,10 @@ sub _init
|
|||
$sql .= $dbh->FOR_UPDATE;
|
||||
}
|
||||
|
||||
# O_o some versions of Perl have a very annoying bug:
|
||||
# class constants like DB_COLUMNS become tainted and the following select dies...
|
||||
trick_taint($sql);
|
||||
|
||||
$object = $dbh->selectrow_hashref($sql, undef, @values);
|
||||
return $object;
|
||||
}
|
||||
|
@ -172,10 +176,8 @@ sub new_from_list
|
|||
my @detainted_ids;
|
||||
foreach my $id (@$id_list)
|
||||
{
|
||||
detaint_natural($id) || ThrowCodeError('param_must_be_numeric', {function => $class . '::new_from_list'});
|
||||
# Too large integers make PostgreSQL crash (FIXME: That's very STRANGE?!!)
|
||||
next if $id > MAX_INT_32;
|
||||
push(@detainted_ids, $id);
|
||||
detaint_natural($id) || ThrowCodeError('param_must_be_numeric', {function => $class . '::new_from_list', id => $id_list->[scalar @detainted_ids]});
|
||||
push @detainted_ids, $id;
|
||||
}
|
||||
# We don't do $invocant->match because some classes have
|
||||
# their own implementation of match which is not compatible
|
||||
|
@ -284,7 +286,7 @@ sub _do_list_select
|
|||
# for the caller. So we copy the array. It's safe to untaint because
|
||||
# they're only used in placeholders here.
|
||||
my @untainted = @{ $values || [] };
|
||||
trick_taint($_) foreach @untainted;
|
||||
trick_taint($_) foreach $sql, @untainted; # O_o another taint workaround
|
||||
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
|
||||
bless ($_, $class) foreach @$objects;
|
||||
return $objects;
|
||||
|
@ -332,6 +334,7 @@ sub set_all
|
|||
sub update
|
||||
{
|
||||
my $self = shift;
|
||||
my (undef, $old_self) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $table = $self->DB_TABLE;
|
||||
|
@ -340,7 +343,7 @@ sub update
|
|||
$dbh->bz_start_transaction();
|
||||
|
||||
# Use a copy of old object
|
||||
my $old_self = $self->new($self->id);
|
||||
$old_self ||= $self->new($self->id);
|
||||
|
||||
my $numeric = $self->NUMERIC_COLUMNS;
|
||||
my $date = $self->DATE_COLUMNS;
|
||||
|
|
|
@ -168,8 +168,12 @@ sub update
|
|||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# This is for future when we'll have a single "save" (create/update) method
|
||||
my $is_new = !$self->id;
|
||||
|
||||
# Don't update the DB if something goes wrong below -> transaction.
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
# Bugzilla::Field::Choice is not a threat as we don't have 'value' field
|
||||
# Yet do not call its update() for the future
|
||||
my ($changes, $old_self) = Bugzilla::Object::update($self, @_);
|
||||
|
@ -266,14 +270,16 @@ sub update
|
|||
}
|
||||
|
||||
# Also update group settings.
|
||||
if ($self->{check_group_controls})
|
||||
if ($is_new || $self->{check_group_controls})
|
||||
{
|
||||
require Bugzilla::Bug;
|
||||
|
||||
my $old_settings = $old_self->group_controls;
|
||||
my $old_settings = !$is_new ? $old_self->group_controls : {};
|
||||
my $new_settings = $self->group_controls;
|
||||
my $timestamp = $dbh->selectrow_array('SELECT NOW()');
|
||||
|
||||
$self->check_open_product;
|
||||
|
||||
foreach my $gid (keys %$new_settings)
|
||||
{
|
||||
my $old_setting = $old_settings->{$gid} || {};
|
||||
|
@ -411,6 +417,37 @@ sub update
|
|||
return $changes;
|
||||
}
|
||||
|
||||
sub check_open_product
|
||||
{
|
||||
my $self = shift;
|
||||
if (Bugzilla->params->{forbid_open_products})
|
||||
{
|
||||
my $new_settings = $self->group_controls;
|
||||
my $has_mandatory = 0;
|
||||
my $has_entry = 0;
|
||||
foreach my $gid (keys %$new_settings)
|
||||
{
|
||||
if ($new_settings->{$gid}->{entry})
|
||||
{
|
||||
$has_entry = 1;
|
||||
}
|
||||
if ($new_settings->{$gid}->{membercontrol} == CONTROLMAPMANDATORY &&
|
||||
$new_settings->{$gid}->{othercontrol} == CONTROLMAPMANDATORY)
|
||||
{
|
||||
$has_mandatory = 1;
|
||||
}
|
||||
}
|
||||
if (!$has_mandatory)
|
||||
{
|
||||
ThrowUserError('product_mandatory_group_required');
|
||||
}
|
||||
if (!$has_entry)
|
||||
{
|
||||
ThrowUserError('product_entry_group_required');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub remove_from_db
|
||||
{
|
||||
my ($self, $params) = @_;
|
||||
|
@ -465,6 +502,9 @@ sub remove_from_db
|
|||
}
|
||||
}
|
||||
|
||||
# Clear external product reference
|
||||
$dbh->do('UPDATE products SET extproduct=NULL WHERE extproduct=?', undef, $self->id);
|
||||
|
||||
$self->SUPER::remove_from_db();
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
@ -609,15 +649,19 @@ use constant is_default => 0;
|
|||
sub _create_bug_group
|
||||
{
|
||||
my $self = shift;
|
||||
my ($create_admin_group, $normal_group) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $group_name = $self->name;
|
||||
my $group_name = ($create_admin_group ? 'admin-' : '') . $self->name;
|
||||
my $i = 1;
|
||||
while (new Bugzilla::Group({ name => $group_name }))
|
||||
{
|
||||
$group_name = $self->name . ($i++);
|
||||
$group_name = ($create_admin_group ? 'admin-' : '') . $self->name . ($i++);
|
||||
}
|
||||
my $group_description = get_text('bug_group_description', { product => $self });
|
||||
my $group_description = get_text(
|
||||
$create_admin_group ? 'admin_group_description' : 'bug_group_description',
|
||||
{ product => $self }
|
||||
);
|
||||
|
||||
my $group = Bugzilla::Group->create({
|
||||
name => $group_name,
|
||||
|
@ -627,9 +671,27 @@ sub _create_bug_group
|
|||
|
||||
# Associate the new group and new product.
|
||||
$dbh->do(
|
||||
'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol)'.
|
||||
' VALUES (?, ?, ?, ?)', undef, $group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA
|
||||
'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol, editcomponents, entry)'.
|
||||
' VALUES (?, ?, ?, ?, ?, ?)', undef, $group->id, $self->id,
|
||||
($create_admin_group ? (0, 0, 1, 0) : (CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0, 1))
|
||||
);
|
||||
|
||||
# Grant current user permission to edit the new group and include him in it
|
||||
$dbh->do(
|
||||
'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) VALUES (?, ?, ?, ?), (?, ?, ?, ?)',
|
||||
undef, Bugzilla->user->id, $group->id, 1, 0, Bugzilla->user->id, $group->id, 0, 0
|
||||
);
|
||||
|
||||
# Allow admin group to grant normal group
|
||||
if ($create_admin_group && $normal_group)
|
||||
{
|
||||
$dbh->do(
|
||||
'INSERT INTO group_group_map (member_id, grantor_id, grant_type) VALUES (?, ?, ?)',
|
||||
undef, $group->id, $normal_group->id, GROUP_BLESS
|
||||
);
|
||||
}
|
||||
|
||||
return $group;
|
||||
}
|
||||
|
||||
sub _create_series
|
||||
|
@ -1007,7 +1069,11 @@ sub flag_types
|
|||
|
||||
sub allows_unconfirmed { return $_[0]->{allows_unconfirmed}; }
|
||||
sub description { return $_[0]->{description}; }
|
||||
sub isactive { return $_[0]->{isactive}; }
|
||||
sub is_active { return $_[0]->{isactive}; }
|
||||
sub votesperuser { return $_[0]->{votesperuser}; }
|
||||
sub maxvotesperbug { return $_[0]->{maxvotesperbug}; }
|
||||
sub votestoconfirm { return $_[0]->{votestoconfirm}; }
|
||||
sub votes_per_user { return $_[0]->{votesperuser}; }
|
||||
sub max_votes_per_bug { return $_[0]->{maxvotesperbug}; }
|
||||
sub votes_to_confirm { return $_[0]->{votestoconfirm}; }
|
||||
|
|
|
@ -104,6 +104,301 @@ sub check
|
|||
return $report;
|
||||
}
|
||||
|
||||
sub _get_names
|
||||
{
|
||||
my ($names, $isnumeric, $field) = @_;
|
||||
|
||||
# These are all the fields we want to preserve the order of in reports.
|
||||
my $f = $field && Bugzilla->get_field($field);
|
||||
if ($f && $f->is_select)
|
||||
{
|
||||
my $values = [ '', map { $_->name } @{ $f->legal_values(1) } ];
|
||||
my %dup;
|
||||
@$values = grep { exists($names->{$_}) && !($dup{$_}++) } @$values;
|
||||
return $values;
|
||||
}
|
||||
elsif ($isnumeric)
|
||||
{
|
||||
# It's not a field we are preserving the order of, so sort it
|
||||
# numerically...
|
||||
sub numerically { $a <=> $b }
|
||||
return [ sort numerically keys %$names ];
|
||||
}
|
||||
else
|
||||
{
|
||||
# ...or alphabetically, as appropriate.
|
||||
return [ sort keys %$names ];
|
||||
}
|
||||
}
|
||||
|
||||
sub get_measures
|
||||
{
|
||||
my $cols = Bugzilla::Search->REPORT_COLUMNS();
|
||||
my $descs = {
|
||||
count => Bugzilla->messages->{field_descs}->{count},
|
||||
times => Bugzilla->messages->{field_descs}->{times},
|
||||
};
|
||||
for my $f (keys %$cols)
|
||||
{
|
||||
if ($cols->{$f}->{numeric})
|
||||
{
|
||||
$descs->{$f} = $cols->{$f}->{title};
|
||||
}
|
||||
}
|
||||
return $descs;
|
||||
}
|
||||
|
||||
sub execute
|
||||
{
|
||||
my $class = shift;
|
||||
my ($ARGS, $runner) = @_;
|
||||
|
||||
my $valid_columns = Bugzilla::Search->REPORT_COLUMNS();
|
||||
|
||||
my $field = {};
|
||||
for (qw(x y z))
|
||||
{
|
||||
my $f = $ARGS->{$_.'_axis_field'} || '';
|
||||
trick_taint($f);
|
||||
if ($f)
|
||||
{
|
||||
if ($valid_columns->{$f})
|
||||
{
|
||||
$field->{$_} = $f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowCodeError("report_axis_invalid", {fld => $_, val => $f});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keys %$field)
|
||||
{
|
||||
ThrowUserError("no_axes_defined");
|
||||
}
|
||||
|
||||
my $width = $ARGS->{width} || 600;
|
||||
my $height = $ARGS->{height} || 350;
|
||||
|
||||
if (defined($width))
|
||||
{
|
||||
(detaint_natural($width) && $width > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
$width <= 2000 || ThrowUserError("chart_too_large");
|
||||
}
|
||||
|
||||
if (defined($height))
|
||||
{
|
||||
(detaint_natural($height) && $height > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
$height <= 2000 || ThrowUserError("chart_too_large");
|
||||
}
|
||||
|
||||
# These shenanigans are necessary to make sure that both vertical and
|
||||
# horizontal 1D tables convert to the correct dimension when you ask to
|
||||
# display them as some sort of chart.
|
||||
my $is_table;
|
||||
if ($ARGS->{format} eq 'table' || $ARGS->{format} eq 'simple')
|
||||
{
|
||||
$is_table = 1;
|
||||
if ($field->{x} && !$field->{y})
|
||||
{
|
||||
# 1D *tables* should be displayed vertically (with a row_field only)
|
||||
$field->{y} = $field->{x};
|
||||
delete $field->{x};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Bugzilla->feature('graphical_reports'))
|
||||
{
|
||||
ThrowCodeError('feature_disabled', { feature => 'graphical_reports' });
|
||||
}
|
||||
if ($field->{y} && !$field->{x})
|
||||
{
|
||||
# 1D *charts* should be displayed horizontally (with an col_field only)
|
||||
$field->{x} = $field->{y};
|
||||
delete $field->{y};
|
||||
}
|
||||
}
|
||||
|
||||
my $measure_alias = {
|
||||
etime => 'estimated_time',
|
||||
rtime => 'remaining_time',
|
||||
wtime => 'interval_time',
|
||||
};
|
||||
my $measures = {
|
||||
count => 'count',
|
||||
};
|
||||
my $old_columns = { %{Bugzilla::Search->COLUMNS($runner)} };
|
||||
# Trick Bugzilla::Search: replace report columns SQL + add '_count' column
|
||||
# FIXME: Remove usage of global variable COLUMNS in search generation code
|
||||
%{Bugzilla::Search->COLUMNS($runner)} = (%{Bugzilla::Search->COLUMNS($runner)}, %{Bugzilla::Search->REPORT_COLUMNS($runner)});
|
||||
Bugzilla::Search->COLUMNS($runner)->{count}->{name} = '1';
|
||||
my $columns = Bugzilla::Search->COLUMNS($runner);
|
||||
for my $column (keys %$columns)
|
||||
{
|
||||
if ($columns->{$column}->{numeric})
|
||||
{
|
||||
$measures->{$column} = $column;
|
||||
}
|
||||
}
|
||||
my $measure = $ARGS->{measure} || '';
|
||||
$measure = $measure_alias->{$measure} || $measure;
|
||||
# Check that $measure is available (+ etime/rtime/wtime is usable only in table mode)
|
||||
if ($measure eq 'times' ? !$is_table : !$measures->{$measure})
|
||||
{
|
||||
$measure = 'count';
|
||||
}
|
||||
# If the user has no access to the measured column, reset it to 'count'
|
||||
if (!Bugzilla::Search->COLUMNS($runner)->{$measure eq 'times' ? 'remaining_time' : $measures->{$measure}})
|
||||
{
|
||||
$measure = 'count';
|
||||
}
|
||||
|
||||
# Validate the values in the axis fields or throw an error.
|
||||
my %a;
|
||||
my @group_by = grep { !($a{$_}++) } values %$field;
|
||||
my @axis_fields = @group_by;
|
||||
for ($measure eq 'times' ? qw(estimated_time remaining_time interval_time) : $measure)
|
||||
{
|
||||
push @axis_fields, $measures->{$_} unless $a{$measures->{$_}};
|
||||
}
|
||||
|
||||
# Clone the params, so that Bugzilla::Search can modify them
|
||||
my $search = new Bugzilla::Search(
|
||||
fields => \@axis_fields,
|
||||
params => { %$ARGS },
|
||||
user => $runner,
|
||||
);
|
||||
my $query = $search->getSQL();
|
||||
$query =
|
||||
"SELECT ".
|
||||
($field->{x} || "''")." x, ".
|
||||
($field->{y} || "''")." y, ".
|
||||
($field->{z} || "''")." z, ".
|
||||
join(', ', map { "SUM($measures->{$_}) $_" } ($measure eq 'times' ? qw(etime rtime wtime) : $measure)).
|
||||
" FROM ($query) _report_table GROUP BY ".join(", ", @group_by);
|
||||
|
||||
$::SIG{TERM} = 'DEFAULT';
|
||||
$::SIG{PIPE} = 'DEFAULT';
|
||||
|
||||
my $results = Bugzilla->dbh->selectall_arrayref($query, {Slice=>{}});
|
||||
|
||||
# We have a hash of hashes for the data itself, and a hash to hold the
|
||||
# row/col/table names.
|
||||
my %data;
|
||||
my %names;
|
||||
|
||||
# Read the bug data and count the bugs for each possible value of row, column and table.
|
||||
#
|
||||
# We detect a numerical field, and sort appropriately, if all the values are numeric.
|
||||
my %isnumeric;
|
||||
|
||||
foreach my $group (@$results)
|
||||
{
|
||||
for (qw(x y z))
|
||||
{
|
||||
$isnumeric{$_} &&= ($group->{$_} =~ /^-?\d+(\.\d+)?$/o);
|
||||
$names{$_}{$group->{$_}} = 1;
|
||||
}
|
||||
$data{$group->{z}}{$group->{x}}{$group->{y}} = $is_table ? $group : $group->{$measure};
|
||||
}
|
||||
|
||||
my @tbl_names = @{_get_names($names{z}, $isnumeric{z}, $field->{z})};
|
||||
my @col_names = @{_get_names($names{x}, $isnumeric{x}, $field->{x})};
|
||||
my @row_names = @{_get_names($names{y}, $isnumeric{y}, $field->{y})};
|
||||
|
||||
# The GD::Graph package requires a particular format of data, so once we've
|
||||
# gathered everything into the hashes and made sure we know the size of the
|
||||
# data, we reformat it into an array of arrays of arrays of data.
|
||||
push @tbl_names, "-total-" if scalar(@tbl_names) > 1;
|
||||
|
||||
my @image_data;
|
||||
foreach my $tbl (@tbl_names)
|
||||
{
|
||||
my @tbl_data;
|
||||
push @tbl_data, \@col_names;
|
||||
foreach my $row (@row_names)
|
||||
{
|
||||
my @col_data;
|
||||
foreach my $col (@col_names)
|
||||
{
|
||||
$data{$tbl}{$col}{$row} ||= {};
|
||||
push @col_data, $data{$tbl}{$col}{$row};
|
||||
if ($tbl ne "-total-")
|
||||
{
|
||||
# This is a bit sneaky. We spend every loop except the last
|
||||
# building up the -total- data, and then last time round,
|
||||
# we process it as another tbl, and push() the total values
|
||||
# into the image_data array.
|
||||
if ($is_table)
|
||||
{
|
||||
for my $m (keys %{$data{$tbl}{$col}{$row}})
|
||||
{
|
||||
next if $m eq 'x' || $m eq 'y' || $m eq 'z';
|
||||
$data{"-total-"}{$col}{$row}{$m} += $data{$tbl}{$col}{$row}{$m};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
|
||||
}
|
||||
}
|
||||
}
|
||||
push @tbl_data, \@col_data;
|
||||
}
|
||||
unshift @image_data, \@tbl_data;
|
||||
}
|
||||
|
||||
# Below a certain width, we don't see any bars, so there needs to be a minimum.
|
||||
if ($width && $ARGS->{format} eq "bar")
|
||||
{
|
||||
my $min_width = (scalar(@col_names) || 1) * 20;
|
||||
if (!$ARGS->{cumulate})
|
||||
{
|
||||
$min_width *= (scalar(@row_names) || 1);
|
||||
}
|
||||
$width = $min_width;
|
||||
}
|
||||
|
||||
my $vars = {};
|
||||
|
||||
$vars->{fields} = $field;
|
||||
|
||||
# for debugging
|
||||
$vars->{query} = $query;
|
||||
|
||||
# We need to keep track of the defined restrictions on each of the
|
||||
# axes, because buglistbase, below, throws them away. Without this, we
|
||||
# get buglistlinks wrong if there is a restriction on an axis field.
|
||||
$vars->{col_vals} = $field->{x} ? http_build_query({ $field->{x} => $ARGS->{$field->{x}} }) : '';
|
||||
$vars->{row_vals} = $field->{y} ? http_build_query({ $field->{y} => $ARGS->{$field->{y}} }) : '';
|
||||
$vars->{tbl_vals} = $field->{z} ? http_build_query({ $field->{z} => $ARGS->{$field->{z}} }) : '';
|
||||
my $a = { %$ARGS };
|
||||
delete $a->{$_} for qw(x_axis_field y_axis_field z_axis_field ctype format query_format measure), @axis_fields;
|
||||
$vars->{buglistbase} = http_build_query($a);
|
||||
|
||||
$vars->{image_data} = \@image_data;
|
||||
$vars->{data} = \%data;
|
||||
$vars->{measure} = $measure;
|
||||
$vars->{tbl_field} = $field->{z};
|
||||
$vars->{col_field} = $field->{x};
|
||||
$vars->{row_field} = $field->{y};
|
||||
$vars->{col_names} = \@col_names;
|
||||
$vars->{row_names} = \@row_names;
|
||||
$vars->{tbl_names} = \@tbl_names;
|
||||
$vars->{width} = $width;
|
||||
$vars->{height} = $height;
|
||||
$vars->{cumulate} = $ARGS->{cumulate} ? 1 : 0;
|
||||
$vars->{x_labels_vertical} = $ARGS->{x_labels_vertical} ? 1 : 0;
|
||||
|
||||
%{Bugzilla::Search->COLUMNS($runner)} = %$old_columns;
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
|
|
@ -463,11 +463,12 @@ sub STATIC_COLUMNS
|
|||
reporter_short => { title => 'Reporter Login' },
|
||||
qa_contact_short => { title => 'QA Contact Login' },
|
||||
# FIXME save aggregated work_time in bugs table and search on it
|
||||
work_time => { name => $actual_time },
|
||||
interval_time => { name => $actual_time, title => 'Period Worktime', noreports => 1 },
|
||||
work_time => { name => $actual_time, numeric => 1 },
|
||||
interval_time => { name => $actual_time, title => 'Period Worktime', numeric => 1 },
|
||||
percentage_complete => {
|
||||
name => "(CASE WHEN $actual_time + bugs.remaining_time = 0.0 THEN 0.0" .
|
||||
" ELSE 100 * ($actual_time / ($actual_time + bugs.remaining_time)) END)",
|
||||
numeric => 1,
|
||||
},
|
||||
'flagtypes.name' => {
|
||||
name =>
|
||||
|
@ -548,6 +549,8 @@ sub STATIC_COLUMNS
|
|||
foreach my $col (qw(assigned_to reporter qa_contact))
|
||||
{
|
||||
my $sql = "map_${col}.login_name";
|
||||
$columns->{$col}->{name} = $sql;
|
||||
$columns->{$col}->{trim_email} = 1;
|
||||
$columns->{$col.'_realname'}->{name} = "map_${col}.realname";
|
||||
$columns->{$col.'_short'}->{name} = $dbh->sql_string_until($sql, $dbh->quote('@'));
|
||||
# Only the qa_contact field can be NULL
|
||||
|
@ -594,6 +597,7 @@ sub STATIC_COLUMNS
|
|||
}
|
||||
elsif ($field->type == FIELD_TYPE_BUG_ID_REV)
|
||||
{
|
||||
# FIXME: Если "обратных" (например, внутренних) багов несколько - join'енные поля по internal bugs работать не будут
|
||||
push @bugid_fields, $field;
|
||||
$columns->{$id}->{name} = "(SELECT ".
|
||||
$dbh->sql_group_concat("rev_$id.bug_id", "', '").
|
||||
|
@ -613,9 +617,19 @@ sub STATIC_COLUMNS
|
|||
" FROM ".$type->REL_TABLE.", $t WHERE $t.".$type->ID_FIELD."=".$type->REL_TABLE.".value_id".
|
||||
" AND ".$type->REL_TABLE.".bug_id=bugs.bug_id)";
|
||||
}
|
||||
elsif ($field->type == FIELD_TYPE_EAV_TEXTAREA)
|
||||
{
|
||||
my $t = "bug_".$field->name;
|
||||
$columns->{$id}->{name} = "$t.value";
|
||||
$columns->{$id}->{joins} = [ "LEFT JOIN $t ON $t.bug_id=bugs.bug_id" ];
|
||||
}
|
||||
elsif ($bug_columns->{$id})
|
||||
{
|
||||
$columns->{$id}->{name} ||= "bugs.$id";
|
||||
if ($field->type == FIELD_TYPE_NUMERIC)
|
||||
{
|
||||
$columns->{$id}->{numeric} = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -645,6 +659,17 @@ sub STATIC_COLUMNS
|
|||
sortkey => 1,
|
||||
};
|
||||
}
|
||||
elsif ($subid eq 'assigned_to' || $subid eq 'reporter' || $subid eq 'qa_contact')
|
||||
{
|
||||
$columns->{$id.'_'.$subid} = {
|
||||
name => "map_${id}_${subid}.login_name",
|
||||
title => $field->description . ' ' . $subfield->description,
|
||||
subid => $subid,
|
||||
sortkey => 1,
|
||||
trim_email => 1,
|
||||
joins => [ @$join, "LEFT JOIN profiles AS map_${id}_${subid} ON bugs_${id}.${subid}=map_${id}_${subid}.userid" ],
|
||||
};
|
||||
}
|
||||
elsif ($subid eq 'product' || $subid eq 'component' || $subid eq 'classification')
|
||||
{
|
||||
$columns->{$id.'_'.$subid} = {
|
||||
|
@ -660,6 +685,17 @@ sub STATIC_COLUMNS
|
|||
],
|
||||
};
|
||||
}
|
||||
elsif ($subid eq 'deadline')
|
||||
{
|
||||
$columns->{$id.'_'.$subid} = {
|
||||
name => $dbh->sql_date_format("bugs_$id.deadline", '%Y-%m-%d'),
|
||||
raw_name => "bugs_$id.deadline",
|
||||
title => $field->description . ' ' . $subfield->description,
|
||||
subid => $subid,
|
||||
sortkey => 1,
|
||||
joins => [ @$join ],
|
||||
};
|
||||
}
|
||||
elsif ($subfield->type == FIELD_TYPE_SINGLE_SELECT)
|
||||
{
|
||||
my $type = $subfield->value_type;
|
||||
|
@ -672,6 +708,32 @@ sub STATIC_COLUMNS
|
|||
sortkey => 1,
|
||||
};
|
||||
}
|
||||
elsif ($subfield->type == FIELD_TYPE_MULTI_SELECT)
|
||||
{
|
||||
my $type = $subfield->value_type;
|
||||
my $t = $type->DB_TABLE;
|
||||
$columns->{$id.'_'.$subid} = {
|
||||
name => "(SELECT ".
|
||||
$dbh->sql_group_concat("$t.".$type->NAME_FIELD, "', '").
|
||||
" FROM ".$type->REL_TABLE.", $t WHERE $t.".$type->ID_FIELD."=".$type->REL_TABLE.".value_id".
|
||||
" AND ".$type->REL_TABLE.".bug_id=bugs_$id".".bug_id)",
|
||||
title => $field->description . ' ' . $subfield->description,
|
||||
joins => [ @$join ],
|
||||
subid => $subid,
|
||||
sortkey => 1,
|
||||
};
|
||||
}
|
||||
elsif ($subfield->type == FIELD_TYPE_MULTI_SELECT)
|
||||
{
|
||||
my $t = 'bug_'.$subfield->name;
|
||||
$columns->{$id.'_'.$subid} = {
|
||||
name => "bugs_$id"."_$t.value",
|
||||
title => $field->description . ' ' . $subfield->description,
|
||||
joins => [ @$join, "LEFT JOIN $t bugs_$id"."_$t ON bugs_$id"."_$t.bug_id=bugs_$id.bug_id" ],
|
||||
subid => $subid,
|
||||
sortkey => 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -707,12 +769,14 @@ sub STATIC_COLUMNS
|
|||
# Now only removes time-tracking fields
|
||||
sub COLUMNS
|
||||
{
|
||||
my ($self, $user) = @_;
|
||||
$user ||= Bugzilla->user;
|
||||
my $cache = Bugzilla->rc_cache_fields;
|
||||
return $cache->{columns} if $cache->{columns};
|
||||
return $cache->{columns}->{$user->id || ''} if $cache->{columns} && $cache->{columns}->{$user->id || ''};
|
||||
my $columns = { %{ STATIC_COLUMNS() } };
|
||||
|
||||
# Non-timetrackers shouldn't see any time-tracking fields
|
||||
if (!Bugzilla->user->is_timetracker)
|
||||
if (!$user->is_timetracker)
|
||||
{
|
||||
delete $columns->{$_} for keys %{TIMETRACKING_FIELDS()};
|
||||
}
|
||||
|
@ -724,51 +788,69 @@ sub COLUMNS
|
|||
{
|
||||
$hint = ' FORCE INDEX (longdescs_bug_id_idx)';
|
||||
}
|
||||
my $priv = (Bugzilla->user->is_insider ? "" : "AND ldc0.isprivate=0 ");
|
||||
my $priv = ($user->is_insider ? "" : "AND ldc0.isprivate=0 ");
|
||||
# Not using JOIN (it could be joined on bug_when=creation_ts),
|
||||
# because it would require COALESCE to an 'isprivate' subquery
|
||||
# for private comments.
|
||||
$columns->{comment0}->{name} =
|
||||
"(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
|
||||
" ORDER BY ldc0.bug_when LIMIT 1)";
|
||||
$columns->{lastcomment}->{name} =
|
||||
"(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
|
||||
" ORDER BY ldc0.bug_when DESC LIMIT 1)";
|
||||
$columns->{comment0} = {
|
||||
%{$columns->{comment0}},
|
||||
name => "(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
|
||||
" ORDER BY ldc0.bug_when LIMIT 1)",
|
||||
};
|
||||
$columns->{lastcomment} = {
|
||||
%{$columns->{lastcomment}},
|
||||
name => "(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
|
||||
" ORDER BY ldc0.bug_when DESC LIMIT 1)",
|
||||
};
|
||||
# Last commenter and last comment time
|
||||
my $login = 'ldp0.login_name';
|
||||
if (!Bugzilla->user->id)
|
||||
if (!$user->id)
|
||||
{
|
||||
$login = $dbh->sql_string_until($login, $dbh->quote('@'));
|
||||
}
|
||||
$columns->{lastcommenter}->{name} =
|
||||
"(SELECT $login FROM longdescs ldc0$hint".
|
||||
$columns->{lastcommenter} = {
|
||||
%{$columns->{lastcommenter}},
|
||||
name => "(SELECT $login FROM longdescs ldc0$hint".
|
||||
" INNER JOIN profiles ldp0 ON ldp0.userid=ldc0.who WHERE ldc0.bug_id = bugs.bug_id $priv".
|
||||
" ORDER BY ldc0.bug_when DESC LIMIT 1)";
|
||||
$priv = (Bugzilla->user->is_insider ? "" : "AND lct.isprivate=0 ");
|
||||
$columns->{last_comment_time}->{name} =
|
||||
"(SELECT MAX(lct.bug_when) FROM longdescs lct$hint WHERE lct.bug_id = bugs.bug_id $priv)";
|
||||
" ORDER BY ldc0.bug_when DESC LIMIT 1)",
|
||||
};
|
||||
$priv = ($user->is_insider ? "" : "AND lct.isprivate=0 ");
|
||||
$columns->{last_comment_time} = {
|
||||
%{$columns->{last_comment_time}},
|
||||
name => "(SELECT MAX(lct.bug_when) FROM longdescs lct$hint WHERE lct.bug_id = bugs.bug_id $priv)",
|
||||
};
|
||||
|
||||
# Hide email domain for anonymous users
|
||||
$columns->{cc}->{name} = "(SELECT ".$dbh->sql_group_concat((Bugzilla->user->id
|
||||
$columns->{cc}->{name} = "(SELECT ".$dbh->sql_group_concat(($user->id
|
||||
? 'profiles.login_name'
|
||||
: $dbh->sql_string_until('profiles.login_name', $dbh->quote('@'))), "','").
|
||||
" cc FROM cc, profiles WHERE cc.bug_id=bugs.bug_id AND cc.who=profiles.userid)";
|
||||
foreach my $col (qw(assigned_to reporter qa_contact))
|
||||
if (!$user->id)
|
||||
{
|
||||
my $sql = "map_${col}.login_name";
|
||||
$columns->{$col}->{name} = Bugzilla->user->id ? $sql : $columns->{$col.'_short'}->{name};
|
||||
foreach my $col (keys %$columns)
|
||||
{
|
||||
if ($columns->{$col}->{trim_email})
|
||||
{
|
||||
$columns->{$col} = {
|
||||
%{$columns->{$col}},
|
||||
name => $dbh->sql_string_until($columns->{$col}->{name}, $dbh->quote('@')),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bugzilla::Hook::process('buglist_columns', { columns => $columns });
|
||||
return $cache->{columns} = $columns;
|
||||
return $cache->{columns}->{$user->id || ''} = $columns;
|
||||
}
|
||||
|
||||
sub REPORT_COLUMNS
|
||||
{
|
||||
my ($self, $user) = @_;
|
||||
$user ||= Bugzilla->user;
|
||||
my $cache = Bugzilla->request_cache;
|
||||
return $cache->{report_columns} if defined $cache->{report_columns};
|
||||
return $cache->{report_columns}->{$user->id || ''} if $cache->{report_columns} && $cache->{report_columns}->{$user->id || ''};
|
||||
|
||||
my $columns = { %{ COLUMNS() } };
|
||||
my $columns = { %{ $self->COLUMNS($user) } };
|
||||
|
||||
# There's no reason to support reporting on unique fields.
|
||||
my @no_report_columns = qw(
|
||||
|
@ -804,11 +886,12 @@ sub REPORT_COLUMNS
|
|||
}
|
||||
}
|
||||
|
||||
return $cache->{report_columns} = $columns;
|
||||
return $cache->{report_columns}->{$user->id || ''} = $columns;
|
||||
}
|
||||
|
||||
# Fields that can be searched on for changes
|
||||
# This is now used only by query.cgi
|
||||
# Depends on current user
|
||||
sub CHANGEDFROMTO_FIELDS
|
||||
{
|
||||
# creation_ts, longdesc, longdescs.isprivate, commenter are treated specially
|
||||
|
@ -932,10 +1015,11 @@ sub FUNCTIONS
|
|||
type => FIELD_TYPE_BUG_ID_REV,
|
||||
obsolete => 0
|
||||
});
|
||||
my $date_fields = join '|', map { $_->name } Bugzilla->get_fields({
|
||||
my @bugid_fields = Bugzilla->get_fields({ type => FIELD_TYPE_BUG_ID });
|
||||
my $date_fields = join '|', ((map { $_->name } Bugzilla->get_fields({
|
||||
type => FIELD_TYPE_DATETIME,
|
||||
obsolete => 0
|
||||
});
|
||||
})), (map { $_->name.'_deadline' } @bugid_fields));
|
||||
$FUNCTIONS = {
|
||||
'blocked|dependson' => {
|
||||
'*' => \&_blocked_dependson,
|
||||
|
@ -1103,11 +1187,11 @@ sub init
|
|||
|
||||
my $H = $self->{params};
|
||||
|
||||
# $self->{user} = User under which the search will be ran
|
||||
# Bugzilla->user = Just current user
|
||||
# $self->{user} = User under which the search will be ran (current user by default)
|
||||
$self->{user} ||= Bugzilla->user;
|
||||
my $user = $self->{user};
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $columns = $self->{columns} = $self->COLUMNS($user);
|
||||
|
||||
my @specialchart;
|
||||
|
||||
|
@ -1310,13 +1394,13 @@ sub init
|
|||
$self->{interval_who} = $override ? $H->{period_who} : $H->{chfieldwho};
|
||||
for ($self->{interval_from}, $self->{interval_to})
|
||||
{
|
||||
$_ = $_ && $_ ne 'now' ? SqlifyDate($_) : undef;
|
||||
$_ = $_ && lc $_ ne 'now' ? SqlifyDate($_) : undef;
|
||||
}
|
||||
for ($self->{interval_who})
|
||||
{
|
||||
$_ = $_ ? ($_ eq '%user%' ? $self->{user} : Bugzilla::User::match_name($_, 1)->[0]) : undef;
|
||||
}
|
||||
COLUMNS->{interval_time}->{name} =
|
||||
$columns->{interval_time}->{name} =
|
||||
"(SELECT COALESCE(SUM(ldtime.work_time),0) FROM longdescs ldtime".
|
||||
" WHERE ldtime.bug_id=bugs.bug_id".
|
||||
($self->{interval_from} ? " AND ldtime.bug_when >= ".$dbh->quote($self->{interval_from}) : '').
|
||||
|
@ -1355,9 +1439,9 @@ sub init
|
|||
}
|
||||
|
||||
# Reset relevance column
|
||||
COLUMNS->{relevance}->{bits} = [];
|
||||
COLUMNS->{relevance}->{joins} = [];
|
||||
COLUMNS->{relevance}->{name} = '(0)';
|
||||
$columns->{relevance}->{bits} = [];
|
||||
$columns->{relevance}->{joins} = [];
|
||||
$columns->{relevance}->{name} = '(0)';
|
||||
|
||||
# Read charts from form hash
|
||||
my @charts;
|
||||
|
@ -1458,7 +1542,7 @@ sub init
|
|||
}
|
||||
}
|
||||
|
||||
if (!$user->is_super_user)
|
||||
if (!$self->{ignore_permissions})
|
||||
{
|
||||
# If there are some terms in the search, assume it's enough
|
||||
# to select bugs and attach security terms without UNION (OR_MANY).
|
||||
|
@ -1524,7 +1608,7 @@ sub init
|
|||
# write the FROM clause.
|
||||
foreach my $orderitem (@inputorder)
|
||||
{
|
||||
BuildOrderBy($special_order, $orderitem, \@orderby);
|
||||
$self->BuildOrderBy($special_order, $orderitem, \@orderby);
|
||||
}
|
||||
|
||||
# Now JOIN the correct tables in the FROM clause.
|
||||
|
@ -1541,16 +1625,16 @@ sub init
|
|||
my @sql_fields;
|
||||
foreach my $field (@fields)
|
||||
{
|
||||
for my $t (@{ COLUMNS->{$field}->{joins} || [] })
|
||||
for my $t (@{ $columns->{$field}->{joins} || [] })
|
||||
{
|
||||
push @supptables, $t if !grep { $_ eq $t } @supptables;
|
||||
}
|
||||
if (COLUMNS->{$field}->{name})
|
||||
if ($columns->{$field}->{name})
|
||||
{
|
||||
my $alias = $field;
|
||||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||||
$alias =~ s/\./_/g;
|
||||
push @sql_fields, COLUMNS->{$field}->{name} . " AS $alias";
|
||||
push @sql_fields, $columns->{$field}->{name} . " AS $alias";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1564,6 +1648,146 @@ sub init
|
|||
$self->{sql} = $query;
|
||||
}
|
||||
|
||||
sub get_columns
|
||||
{
|
||||
my $class = shift;
|
||||
my ($params, $user) = @_;
|
||||
my $displaycolumns;
|
||||
|
||||
if (defined $params->{columnlist} && $params->{columnlist} ne 'all')
|
||||
{
|
||||
$displaycolumns = [ split(/[ ,]+/, $params->{columnlist}) ];
|
||||
}
|
||||
elsif (defined Bugzilla->cookies->{COLUMNLIST})
|
||||
{
|
||||
$displaycolumns = [ split(/ /, Bugzilla->cookies->{COLUMNLIST}) ];
|
||||
}
|
||||
else
|
||||
{
|
||||
# Use the default list of columns.
|
||||
$displaycolumns = [ DEFAULT_COLUMN_LIST ];
|
||||
}
|
||||
|
||||
# Figure out whether or not the user is doing a fulltext search. If not,
|
||||
# we'll remove the relevance column from the lists of columns to display
|
||||
# and order by, since relevance only exists when doing a fulltext search.
|
||||
my $fulltext = $params->{content} ||
|
||||
grep { $params->{$_} eq 'content' && /^field(\d+-\d+-\d+)/ && $params->{"value$1"} } keys %$params;
|
||||
|
||||
my $columns = $class->COLUMNS($user);
|
||||
$_ = $class->COLUMN_ALIASES->{$_} || $_ for @$displaycolumns;
|
||||
@$displaycolumns = grep($columns->{$_} && trick_taint($_), @$displaycolumns);
|
||||
if (!$user->is_timetracker)
|
||||
{
|
||||
@$displaycolumns = grep { !TIMETRACKING_FIELDS->{$_} } @$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 'bug_id', @$displaycolumns);
|
||||
|
||||
# The bug ID is always selected because bug IDs are always displayed.
|
||||
# Severity, priority, resolution and status are required for buglist
|
||||
# CSS classes.
|
||||
# Display columns are selected because otherwise we could not display them.
|
||||
my $selectcolumns = { map { $_ => 1 } (qw(bug_id bug_severity priority bug_status resolution product), @$displaycolumns) };
|
||||
|
||||
# Make sure that the login_name version of a field is always also
|
||||
# requested if the realname version is requested, so that we can
|
||||
# display the login name when the realname is empty.
|
||||
my @realname_fields = grep(/_realname$|_short$/, @$displaycolumns);
|
||||
foreach my $item (@realname_fields)
|
||||
{
|
||||
my $login_field = $item;
|
||||
$login_field =~ s/_realname$|_short$//;
|
||||
$selectcolumns->{$login_field} = 1;
|
||||
}
|
||||
|
||||
return ($displaycolumns, [ keys %$selectcolumns ]);
|
||||
}
|
||||
|
||||
sub get_order
|
||||
{
|
||||
my $class = shift;
|
||||
my ($params, $user) = @_;
|
||||
my $order = $params->{order} || "";
|
||||
|
||||
# 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 ($order = Bugzilla->cookies->{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 $columns = $class->COLUMNS($user);
|
||||
my $fulltext = $params->{content} ||
|
||||
grep { $params->{$_} eq 'content' && /^field(\d+-\d+-\d+)/ && $params->{"value$1"} } keys %$params;
|
||||
|
||||
my $old_orders = {
|
||||
'' => 'bug_status,priority,assigned_to,bug_id', # Default
|
||||
'bug number' => 'bug_id',
|
||||
'importance' => 'priority,bug_severity,bug_id',
|
||||
'assignee' => 'assigned_to,bug_status,priority,bug_id',
|
||||
'last changed' => 'delta_ts,bug_status,priority,assigned_to,bug_id',
|
||||
};
|
||||
my @invalid_fragments;
|
||||
my @orderstrings;
|
||||
if ($order)
|
||||
{
|
||||
# Convert the value of the "order" form field into a list of columns
|
||||
# by which to sort the results.
|
||||
if ($old_orders->{lc $order})
|
||||
{
|
||||
@orderstrings = split /,/, $old_orders->{lc $order};
|
||||
}
|
||||
else
|
||||
{
|
||||
trick_taint($order);
|
||||
# A custom list of columns. Make sure each column is valid.
|
||||
foreach my $fragment (split(/,/, $order))
|
||||
{
|
||||
$fragment = trim($fragment);
|
||||
next unless $fragment;
|
||||
my ($column_name, $direction) = Bugzilla::Search::split_order_term($fragment);
|
||||
$column_name = Bugzilla::Search::translate_old_column($column_name);
|
||||
|
||||
# Special handlings for certain columns
|
||||
next if $column_name eq 'relevance' && !$fulltext;
|
||||
|
||||
# If we are sorting by votes, sort in descending order if
|
||||
# no explicit sort order was given.
|
||||
if ($column_name eq 'votes' && !$direction)
|
||||
{
|
||||
$direction = "DESC";
|
||||
}
|
||||
|
||||
if (exists $columns->{$column_name})
|
||||
{
|
||||
$direction = " $direction" if $direction;
|
||||
push @orderstrings, "$column_name$direction";
|
||||
}
|
||||
else
|
||||
{
|
||||
push @invalid_fragments, $fragment;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$order = $old_orders->{''} if !$order;
|
||||
return (\@orderstrings, \@invalid_fragments);
|
||||
}
|
||||
|
||||
# Return query-wide equality operators
|
||||
sub get_equalities
|
||||
{
|
||||
|
@ -1580,10 +1804,11 @@ sub sv_quote
|
|||
# HTML search description, moved here from templates
|
||||
sub search_description_html
|
||||
{
|
||||
my $self = shift;
|
||||
my ($exp, $debug, $inner) = @_;
|
||||
my $opdescs = Bugzilla->messages->{operator_descs};
|
||||
my $fdescs = Bugzilla->messages->{field_descs};
|
||||
$exp = $exp->{terms_without_security} if ref $exp eq 'Bugzilla::Search';
|
||||
$exp = $self->{terms_without_security} if !$exp;
|
||||
my $html = '';
|
||||
if (ref $exp eq 'ARRAY')
|
||||
{
|
||||
|
@ -1593,7 +1818,7 @@ sub search_description_html
|
|||
for my $i (1 .. $#$exp)
|
||||
{
|
||||
$html .= " <li class='_".lc($op)."'>$op</li>" if $i > 1;
|
||||
$html .= ' <li>'.search_description_html($exp->[$i], $debug, 1).'</li>';
|
||||
$html .= ' <li>'.$self->search_description_html($exp->[$i], $debug, 1).'</li>' if $exp->[$i];
|
||||
}
|
||||
$html .= '</ul>';
|
||||
}
|
||||
|
@ -1612,7 +1837,7 @@ sub search_description_html
|
|||
if ($d->[0])
|
||||
{
|
||||
my $a = COLUMN_ALIASES->{$d->[0]} || $d->[0];
|
||||
$html .= '<span class="search_field">'.html_quote(COLUMNS->{$a}->{title} || $fdescs->{$a} || $a).':</span>';
|
||||
$html .= '<span class="search_field">'.html_quote($self->{columns}->{$a}->{title} || $fdescs->{$a} || $a).':</span>';
|
||||
}
|
||||
$html .= ' '.$opdescs->{not} if $neg;
|
||||
$html .= ' '.$opdescs->{$d->[1]} if !SEARCH_HIDDEN_OPERATORS->{$d->[1]};
|
||||
|
@ -1642,7 +1867,7 @@ sub search_description_html
|
|||
if ($f && @$f)
|
||||
{
|
||||
$s = $opdescs->{desc_fields};
|
||||
$s =~ s/\$1/sv_quote(join(', ', map { COLUMNS->{$_}->{title} || $_ } @$f))/es;
|
||||
$s =~ s/\$1/sv_quote(join(', ', map { $self->{columns}->{$_}->{title} || $_ } @$f))/es;
|
||||
push @l, $s;
|
||||
}
|
||||
$html .= join(', ', @l);
|
||||
|
@ -1839,6 +2064,7 @@ sub pronoun
|
|||
# order. That is, that we wanted "A DESC", not "A".
|
||||
sub BuildOrderBy
|
||||
{
|
||||
my $self = shift;
|
||||
my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
|
||||
|
||||
my ($orderfield, $orderdirection) = split_order_term($orderitem);
|
||||
|
@ -1865,16 +2091,16 @@ sub BuildOrderBy
|
|||
{
|
||||
# DESC on a field with non-standard sort order means
|
||||
# "reverse the normal order for each field that we map to."
|
||||
BuildOrderBy($special_order, $subitem, $stringlist,
|
||||
$orderdirection =~ m/desc/i);
|
||||
$self->BuildOrderBy($special_order, $subitem, $stringlist, $orderdirection =~ m/desc/i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
# Aliases cannot contain dots in them. We convert them to underscores.
|
||||
$orderfield =~ s/\./_/g if exists COLUMNS->{$orderfield};
|
||||
|
||||
$orderfield =~ s/\./_/g if exists $self->{columns}->{$orderfield};
|
||||
push @$stringlist, trim($orderfield . ' ' . $orderdirection);
|
||||
}
|
||||
}
|
||||
|
||||
# Splits out "asc|desc" from a sort order item.
|
||||
sub split_order_term
|
||||
|
@ -1912,7 +2138,7 @@ sub translate_old_column
|
|||
my $rc = Bugzilla->request_cache;
|
||||
if (!$rc->{columns_by_sql_code})
|
||||
{
|
||||
my $col = COLUMNS();
|
||||
my $col = Bugzilla::Search->COLUMNS;
|
||||
$rc->{columns_by_sql_code} = {
|
||||
map { $col->{$_}->{name} => $_ } grep { $col->{$_}->{name} } keys %$col
|
||||
};
|
||||
|
@ -1979,7 +2205,7 @@ sub run_chart
|
|||
$self->{field} = COLUMN_ALIASES->{$self->{field}} if COLUMN_ALIASES->{$self->{field}};
|
||||
# chart -1 is generated by other code above, not from the user-
|
||||
# submitted form, so we'll blindly accept any values in chart -1
|
||||
if (!COLUMNS->{$self->{field}} && $check_field_name)
|
||||
if (!$self->{columns}->{$self->{field}} && $check_field_name)
|
||||
{
|
||||
ThrowUserError('invalid_field_name', { field => $self->{field} });
|
||||
}
|
||||
|
@ -1993,15 +2219,15 @@ sub run_chart
|
|||
# already know about it), or it was in %chartfields, so it is
|
||||
# a valid field name, which means that it's ok.
|
||||
trick_taint($self->{field});
|
||||
if (!ref $self->{value})
|
||||
if (!ref $self->{value} || ref $self->{value} eq 'ARRAY')
|
||||
{
|
||||
$self->{quoted} = Bugzilla->dbh->quote($self->{value});
|
||||
$self->{quoted} = Bugzilla->dbh->quote(ref $self->{value} ? $self->{value}->[0] : $self->{value});
|
||||
trick_taint($self->{quoted});
|
||||
}
|
||||
if (COLUMNS->{$self->{field}}->{name})
|
||||
if ($self->{columns}->{$self->{field}}->{name})
|
||||
{
|
||||
$self->{fieldsql} = COLUMNS->{$self->{field}}->{name};
|
||||
if (my $j = COLUMNS->{$self->{field}}->{joins})
|
||||
$self->{fieldsql} = $self->{columns}->{$self->{field}}->{name};
|
||||
if (my $j = $self->{columns}->{$self->{field}}->{joins})
|
||||
{
|
||||
# Automatically adds table joins when converted to string
|
||||
$self->{fieldsql} = bless [ $self->{fieldsql}, $j, $self->{supptables}, $self->{suppseen} ], 'Bugzilla::Search::Code';
|
||||
|
@ -2157,7 +2383,7 @@ sub changed
|
|||
# User is searching for a comment with specific text
|
||||
$ld_term->{where} .= ' AND '.$dbh->sql_iposition("$ld.thetext", $added);
|
||||
}
|
||||
elsif ($f{'longdescs.isprivate'} && $self->user->is_insider)
|
||||
elsif ($f{'longdescs.isprivate'} && $self->{user}->is_insider)
|
||||
{
|
||||
# Insider is searching for a comment with specific privacy
|
||||
$ld_term->{where} .= " AND $ld.isprivate = ".($v->{value} ? 1 : 0);
|
||||
|
@ -2373,6 +2599,12 @@ sub _content_matches
|
|||
}
|
||||
$text =~ s/((?<!\\)(?:\\\\)*)([$pattern_part])/$1\\$2/gs;
|
||||
$text =~ s/(?<=[\s-])-(?=[\s-])/\\-/gso;
|
||||
$text =~ s/\s+$//so;
|
||||
if (!$text)
|
||||
{
|
||||
ThrowUserError('invalid_fulltext_query');
|
||||
return;
|
||||
}
|
||||
$text = ($self->{user}->is_insider ? '@(short_desc,comments,comments_private) ' : '@(short_desc,comments) ') . $text;
|
||||
if ($dbh->isa('Bugzilla::DB::Mysql') &&
|
||||
Bugzilla->localconfig->{sphinxse_port})
|
||||
|
@ -2381,7 +2613,8 @@ sub _content_matches
|
|||
$text =~ s/;/\\;/gso;
|
||||
$text =~ s/\\/\\\\/gso;
|
||||
# Space is needed after $text so Sphinx doesn't escape ";"
|
||||
$text = "$text ;mode=extended;limit=1000;fieldweights=short_desc,5,comments,1,comments_private,1";
|
||||
my $maxm = Bugzilla->params->{sphinx_max_matches} || 1000;
|
||||
$text = "$text ;mode=extended;limit=$maxm;maxmatches=$maxm;fieldweights=short_desc,5,comments,1,comments_private,1";
|
||||
$self->{term} = {
|
||||
table => "bugs_fulltext_sphinx $table",
|
||||
where => "$table.query=".$dbh->quote($text),
|
||||
|
@ -2389,9 +2622,9 @@ sub _content_matches
|
|||
};
|
||||
if (!$self->{negated})
|
||||
{
|
||||
push @{COLUMNS->{relevance}->{joins}}, "LEFT JOIN bugs_fulltext_sphinx $table ON $table.id=bugs.bug_id AND $table.query=".$dbh->quote($text);
|
||||
push @{COLUMNS->{relevance}->{bits}}, "$table.weight";
|
||||
COLUMNS->{relevance}->{name} = '('.join("+", @{COLUMNS->{relevance}->{bits}}).')';
|
||||
push @{$self->{columns}->{relevance}->{joins}}, "LEFT JOIN bugs_fulltext_sphinx $table ON $table.id=bugs.bug_id AND $table.query=".$dbh->quote($text);
|
||||
push @{$self->{columns}->{relevance}->{bits}}, "$table.weight";
|
||||
$self->{columns}->{relevance}->{name} = '('.join("+", @{$self->{columns}->{relevance}->{bits}}).')';
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -2407,8 +2640,8 @@ sub _content_matches
|
|||
if (@$ids && !$self->{negated})
|
||||
{
|
||||
# Pass relevance (weight) values via an overlong (CASE ... END)
|
||||
push @{COLUMNS->{relevance}->{bits}}, '(case'.join('', map { ' when bugs.bug_id='.$_->[0].' then '.$_->[1] } @$ids).' end)';
|
||||
COLUMNS->{relevance}->{name} = '('.join("+", @{COLUMNS->{relevance}->{bits}}).')';
|
||||
push @{$self->{columns}->{relevance}->{bits}}, '(case'.join('', map { ' when bugs.bug_id='.$_->[0].' then '.$_->[1] } @$ids).' end)';
|
||||
$self->{columns}->{relevance}->{name} = '('.join("+", @{$self->{columns}->{relevance}->{bits}}).')';
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
@ -2440,8 +2673,8 @@ sub _content_matches
|
|||
# this adds more terms to the relevance sql.
|
||||
if (!$self->{negated})
|
||||
{
|
||||
push @{COLUMNS->{relevance}->{bits}}, @terms[grep { $_&1 } 0..$#terms];
|
||||
COLUMNS->{relevance}->{name} = $dbh->sql_fulltext_relevance_sum(COLUMNS->{relevance}->{bits});
|
||||
push @{$self->{columns}->{relevance}->{bits}}, @terms[grep { $_&1 } 0..$#terms];
|
||||
$self->{columns}->{relevance}->{name} = $dbh->sql_fulltext_relevance_sum($self->{columns}->{relevance}->{bits});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2449,7 +2682,11 @@ sub _timestamp_compare
|
|||
{
|
||||
my $self = shift;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$self->{fieldsql} = 'bugs.'.$self->{field};
|
||||
if ($self->{columns}->{$self->{field}}->{joins})
|
||||
{
|
||||
push @{$self->{supptables}}, @{$self->{columns}->{$self->{field}}->{joins}};
|
||||
}
|
||||
$self->{fieldsql} = $self->{columns}->{$self->{field}}->{raw_name} || 'bugs.'.$self->{field};
|
||||
if ($self->{value} =~ /^[+-]?\d+[dhwmy]$/is)
|
||||
{
|
||||
$self->{value} = SqlifyDate($self->{value});
|
||||
|
@ -2886,21 +3123,7 @@ sub _equals
|
|||
}
|
||||
else
|
||||
{
|
||||
$self->{term} = "$self->{fieldsql} = $self->{quoted}";
|
||||
}
|
||||
}
|
||||
|
||||
sub _notequals
|
||||
{
|
||||
my $self = shift;
|
||||
if ($self->{value} eq '')
|
||||
{
|
||||
$self->{term} = "($self->{fieldsql} != $self->{quoted} AND $self->{fieldsql} IS NOT NULL)";
|
||||
}
|
||||
else
|
||||
{
|
||||
$self->{allow_null} = 1;
|
||||
$self->{term} = "$self->{fieldsql} != $self->{quoted}";
|
||||
$self->{term} = "($self->{fieldsql} = $self->{quoted} AND $self->{fieldsql} IS NOT NULL)";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3117,21 +3340,32 @@ sub _in_search_results
|
|||
{
|
||||
my $self = shift;
|
||||
my $v = $self->{value};
|
||||
my $sharer = $self->{params}->{sharer_id};
|
||||
my $sharer = $self->{params}->{sharer_id} || $self->{user}->id;
|
||||
# Do not check permissions for sharer's queries
|
||||
my $m = 'new';
|
||||
if ($v =~ /^(.*)<([^>]*)>/so)
|
||||
{
|
||||
# Allow to match on shared searches via 'SearchName <user@domain.com>' syntax
|
||||
$v = $1;
|
||||
$sharer = Bugzilla::User::login_to_id(trim($2), THROW_ERROR);
|
||||
my $new_sharer = Bugzilla::User::login_to_id(trim($2), THROW_ERROR);
|
||||
# Check permissions if the specified user is not the query's sharer
|
||||
$m = 'check' if $new_sharer != $sharer;
|
||||
$sharer = $new_sharer;
|
||||
}
|
||||
my $query = Bugzilla::Search::Saved->check({
|
||||
my $query = Bugzilla::Search::Saved->$m({
|
||||
name => trim($v),
|
||||
user => $sharer,
|
||||
})->query;
|
||||
runner => $self->{user},
|
||||
});
|
||||
if (!$query)
|
||||
{
|
||||
ThrowUserError('object_does_not_exist', { name => trim($v), user => $sharer, class => 'Bugzilla::Search::Saved' });
|
||||
}
|
||||
$query = $query->query;
|
||||
my $search = new Bugzilla::Search(
|
||||
params => http_decode_query($query),
|
||||
fields => [ "bugs.bug_id" ],
|
||||
user => Bugzilla->user,
|
||||
user => $self->{user},
|
||||
);
|
||||
my $sqlquery = $search->bugid_query;
|
||||
my $t = "ins_".$self->{sequence};
|
||||
|
@ -3141,9 +3375,19 @@ sub _in_search_results
|
|||
$self->{term}->{bugid_field} = "$t.bug_id";
|
||||
}
|
||||
else
|
||||
{
|
||||
my $f = Bugzilla->get_field($self->{field});
|
||||
if ($f && ($f->type == FIELD_TYPE_BUG_ID || $f->type == FIELD_TYPE_BUG_ID_REV) ||
|
||||
$self->{field} eq 'blocked' || $self->{field} eq 'dependson' || $self->{field} eq 'dup_id')
|
||||
{
|
||||
$self->{term}->{where} = "$self->{fieldsql} = $t.bug_id";
|
||||
$self->{term}->{notnull_field} = "$t.bug_id";
|
||||
$self->{term}->{supp} = [ @{$self->{supptables}} ];
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowUserError('in_search_needs_bugid_field', { field => $self->{field} });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3188,7 +3432,7 @@ sub negate_expression
|
|||
if (ref $q eq 'HASH')
|
||||
{
|
||||
$q->{neg} = !$q->{neg};
|
||||
$q->{allow_null} = ($q->{allow_null} || 0) == 1 ? 0 : $q->{allow_null};
|
||||
$q->{allow_null} = ($q->{allow_null} || 0) == 1 ? 0 : 1;
|
||||
$q->{description}->[1] = NEGATE_ALL_OPERATORS->{$q->{description}->[1]} || $q->{description}->[1];
|
||||
}
|
||||
elsif (!ref $q)
|
||||
|
|
|
@ -102,10 +102,12 @@ sub new
|
|||
sub check
|
||||
{
|
||||
my $class = shift;
|
||||
my $search = $class->SUPER::check(@_);
|
||||
my $user = Bugzilla->user;
|
||||
my ($param) = @_;
|
||||
my $search = $class->SUPER::check($param);
|
||||
my $user = $param->{runner} || Bugzilla->user;
|
||||
return $search if $search->user->id == $user->id;
|
||||
if (!$search->shared_with_group || !$user->in_group($search->shared_with_group))
|
||||
if (!$user->in_group('admin') &&
|
||||
(!$search->shared_with_group || !$user->in_group($search->shared_with_group)))
|
||||
{
|
||||
ThrowUserError('missing_query', {
|
||||
queryname => $search->name,
|
||||
|
|
|
@ -171,13 +171,17 @@ sub makeTables
|
|||
{
|
||||
if (scalar($line =~ s/(\t+|│+)/$1/gso) > 0)
|
||||
{
|
||||
$line =~ s/^\s*│\s*//;
|
||||
$table->add(split /\t+|\s*│+\s*/, $line);
|
||||
$line =~ /[─┌┐└┘├┴┬┤┼]+/gso; # legacy ascii tables
|
||||
$line =~ s/^\s*│\s*//s;
|
||||
$line =~ s/\s*│\s*$//s;
|
||||
$line = [ split /\t+\s*|\s*│+\s*/, $line ];
|
||||
$line = '<tr><td>'.join('</td><td>', @$line).'</td></tr>';
|
||||
$table .= "\n".$line;
|
||||
next;
|
||||
}
|
||||
else
|
||||
{
|
||||
$wrappedcomment .= "\0\1".$table->render."\0\1";
|
||||
$wrappedcomment .= "<table class='bz_fmt_table'>$table</table>\n";
|
||||
$table = undef;
|
||||
}
|
||||
}
|
||||
|
@ -185,21 +189,19 @@ sub makeTables
|
|||
if ($n > 1 && length($line) < MAX_TABLE_COLS)
|
||||
{
|
||||
# Table
|
||||
$line =~ s/^\s*│\s*//;
|
||||
$line =~ s/\s*│\s*$//;
|
||||
$table = Text::TabularDisplay::Utf8->new;
|
||||
$table->add(split /\t+|\s*│+\s*/, $line);
|
||||
$line =~ /[─┌┐└┘├┴┬┤┼]+/gso; # legacy ascii tables
|
||||
$line =~ s/^\s*│\s*//s;
|
||||
$line =~ s/\s*│\s*$//s;
|
||||
$line = [ split /\t+\s*|\s*│+\s*/, $line ];
|
||||
$table = "<tr><td>".join('</td><td>', @$line)."</td></tr>\n";
|
||||
next;
|
||||
}
|
||||
unless ($line =~ /^[│─┌┐└┘├┴┬┤┼].*[│─┌┐└┘├┴┬┤┼]$/iso)
|
||||
{
|
||||
$line =~ s/\t/ /gso;
|
||||
}
|
||||
$wrappedcomment .= $line . "\n";
|
||||
}
|
||||
if ($table)
|
||||
{
|
||||
$wrappedcomment .= "\0\1".$table->render."\0\1";
|
||||
$wrappedcomment .= "<table class='bz_fmt_table'>$table</table>\n";
|
||||
}
|
||||
return $wrappedcomment;
|
||||
}
|
||||
|
@ -215,8 +217,6 @@ sub quoteUrls
|
|||
my ($text, $bug, $comment) = (@_);
|
||||
return $text unless $text;
|
||||
|
||||
$text = makeTables($text);
|
||||
|
||||
# We use /g for speed, but uris can have other things inside them
|
||||
# (http://foo/bug#3 for example). Filtering that out filters valid
|
||||
# bug refs out, so we have to do replacements.
|
||||
|
@ -289,15 +289,12 @@ sub quoteUrls
|
|||
~gesix;
|
||||
}
|
||||
|
||||
$text =~ s~
|
||||
\b((?:$safe_protocols): # The protocol:
|
||||
[^\s<>\"]+ # Any non-whitespace
|
||||
[\w\/]) # so that we end in \w or /
|
||||
~
|
||||
# the protocol + non-whitespace + recursive braces + ending in [\w/~=)]
|
||||
$text =~ s/\b((?:$safe_protocols):((?>[^\s<>\"\(\)]+|\((?:(?2)|\")*\)))+(?<=[\w\/~=)]))/
|
||||
($tmp = html_quote($1)) &&
|
||||
($things[$count++] = "<a href=\"$tmp\">$tmp</a>") &&
|
||||
($things[$count++] = "<a href=\"$tmp\">$tmp<\/a>") &&
|
||||
("\0\0" . ($count-1) . "\0\0")
|
||||
~gesox;
|
||||
/gesox;
|
||||
|
||||
if ($custom_proto && %$custom_proto)
|
||||
{
|
||||
|
@ -340,11 +337,14 @@ sub quoteUrls
|
|||
my $q = { '<' => '<', '>' => '>', '&' => '&', '"' => '"' };
|
||||
my $safe_tags = '(?:b|i|u|hr|marquee|s|strike|strong|small|big|sub|sup|tt|em|cite|font(?:\s+color=["\']?(?:#[0-9a-f]{3,6}|[a-z]+)["\']?)?)';
|
||||
my $block_tags = '(?:h[1-6]|center|ol|ul|li)';
|
||||
$text =~ s/<pre>((?:.*?(?:<pre>(?1)<\/pre>)?)*)<\/pre>|\s*(<\/?$block_tags>)\s*|(<\/?$safe_tags>)|([<>&\"])/$4 ? $q->{$4} : lc($1 eq '' ? ($2 eq '' ? $3 : $2) : html_quote($1))/geiso;
|
||||
$text =~ s/<pre>((?:(?>.+?)(?:<pre>(?1)<\/pre>)?)+)?<\/pre>|\s*(<\/?$block_tags>)\s*|(<\/?$safe_tags>)|([<>&\"])/$4 ? $q->{$4} : ($1 eq '' ? lc($2 eq '' ? $3 : $2) : html_quote($1))/geiso;
|
||||
|
||||
# Replace nowrap markers (\1\0\1)
|
||||
$text =~ s/\x01\x00\x01(.*?)\x01\x00\x01/<div style="white-space: nowrap">$1<\/div>/gso;
|
||||
|
||||
# Replace tables
|
||||
$text = makeTables($text);
|
||||
|
||||
# Color quoted text
|
||||
$text = makeCitations($text);
|
||||
|
||||
|
@ -422,10 +422,15 @@ sub unquote_wiki_url
|
|||
my $anchor = '';
|
||||
if ($article =~ s/#(.*)$//so)
|
||||
{
|
||||
my $a;
|
||||
$anchor = $1;
|
||||
# decode MediaWiki page section name
|
||||
$anchor =~ tr/./%/;
|
||||
$anchor = url_decode($anchor);
|
||||
# decode MediaWiki page section name (only correct UTF8 sequences)
|
||||
$anchor =~ s/((?:
|
||||
\.[0-7][A-F0-9]|
|
||||
\.[CD][A-F0-9]\.[89AB][A-F0-9]|
|
||||
\.E[A-F0-9](?:\.[89AB][A-F0-9]){2}|
|
||||
\.F[0-7](?:\.[89AB][A-F0-9]){3}
|
||||
)+)/($a = $1), ($a =~ tr!.!\%!), (url_decode($a))/gesx;
|
||||
$anchor =~ tr/_/ /;
|
||||
}
|
||||
$article =~ s/&.*$//so if $wikiurl =~ /title=$/so;
|
||||
|
@ -434,9 +439,12 @@ sub unquote_wiki_url
|
|||
Encode::_utf8_on($linkurl);
|
||||
Encode::_utf8_on($article);
|
||||
Encode::_utf8_on($anchor);
|
||||
if (utf8::valid($article) && utf8::valid($anchor))
|
||||
{
|
||||
$linkurl = '<a href="'.html_quote($wikiurl.$linkurl).'">'.$wikiname.':[['.$article.($anchor eq '' ? '' : '#'.$anchor).']]</a>';
|
||||
return $linkurl;
|
||||
}
|
||||
}
|
||||
$linkurl = html_quote($wikiurl.$linkurl);
|
||||
return "<a href=\"$linkurl\">$linkurl</a>";
|
||||
}
|
||||
|
@ -948,6 +956,21 @@ sub create
|
|||
# html_quote in the form of a function
|
||||
html => \&html_quote,
|
||||
|
||||
# escape regular expression characters
|
||||
regex_escape => sub
|
||||
{
|
||||
my ($s) = @_;
|
||||
return "\Q$s\E";
|
||||
},
|
||||
|
||||
# escape regular expression replacement characters
|
||||
replacement_escape => sub
|
||||
{
|
||||
my ($s) = @_;
|
||||
$s =~ s/([\\\$])/\\$1/gso;
|
||||
return $s;
|
||||
},
|
||||
|
||||
# HTML <select>
|
||||
# html_select(name, <selected value>, <values>, [<value names>], [<attr_hash>])
|
||||
# <values> may be one of:
|
||||
|
|
|
@ -653,17 +653,16 @@ sub get_products_by_permission
|
|||
sub get_editable_products
|
||||
{
|
||||
my ($self, $classification_id) = @_;
|
||||
my $sql = "SELECT DISTINCT products.id FROM products";
|
||||
my $t = "WHERE";
|
||||
my $sql = "SELECT DISTINCT p.id FROM products p";
|
||||
if (!$self->in_group('editcomponents'))
|
||||
{
|
||||
$sql .= ", group_control_map WHERE editcomponents=1 AND group_id IN (".$self->groups_as_string.")";
|
||||
$t = "AND";
|
||||
$sql .= " INNER JOIN group_control_map gm ON gm.product_id=p.id".
|
||||
" AND gm.editcomponents=1 AND gm.group_id IN (".$self->groups_as_string.")";
|
||||
}
|
||||
if ($classification_id)
|
||||
{
|
||||
# restrict product list by classification
|
||||
$sql .= " $t classification_id=".int($classification_id);
|
||||
$sql .= " WHERE classification_id=".int($classification_id);
|
||||
}
|
||||
return Bugzilla::Product->new_from_list(Bugzilla->dbh->selectcol_arrayref($sql) || []);
|
||||
}
|
||||
|
@ -971,7 +970,7 @@ sub check_can_admin_product
|
|||
# First make sure the product name is valid.
|
||||
my $product = Bugzilla::Product::check_product($product_name);
|
||||
|
||||
($self->in_group('editcomponents', $product->id) && $self->can_see_product($product->name))
|
||||
$self->in_group('editcomponents', $product->id)
|
||||
|| $self->in_group('editcomponents')
|
||||
|| ThrowUserError('product_admin_denied', {product => $product->name});
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ use List::Util qw(first);
|
|||
use Scalar::Util qw(tainted blessed);
|
||||
use Template::Filters;
|
||||
use Text::Wrap;
|
||||
use Text::TabularDisplay::Utf8;
|
||||
use JSON;
|
||||
|
||||
use Data::Dumper qw(Dumper);
|
||||
|
@ -457,17 +456,18 @@ sub wrap_comment # makeParagraphs
|
|||
my $tmp;
|
||||
my $text = '';
|
||||
my $block_tags = '(?:div|h[1-6]|center|ol|ul|li)';
|
||||
my $table_tags = '(?:table|tbody|thead|tr|td|th)';
|
||||
while ($input ne '')
|
||||
{
|
||||
# Convert double line breaks to new paragraphs
|
||||
if ($input =~ m!\n\s*\n|(</?$block_tags[^<>]*>)!so)
|
||||
if ($input =~ m!\n\s*\n|(</?$table_tags[^<>]*>)|(</?$block_tags[^<>]*>)!so)
|
||||
{
|
||||
@m = (substr($input, 0, $-[0]), $1);
|
||||
@m = (substr($input, 0, $-[0]), $1||$2, $1);
|
||||
$input = substr($input, $+[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
@m = ($input, '');
|
||||
@m = ($input, '', '');
|
||||
$input = '';
|
||||
}
|
||||
if ($m[0] ne '')
|
||||
|
@ -476,7 +476,7 @@ sub wrap_comment # makeParagraphs
|
|||
$m[0] =~ s/^\s*\n//s;
|
||||
$m[0] =~ s/^([ \t]+)/$tmp = $1; s!\t! !g; $tmp/emog;
|
||||
$m[0] =~ s/(<[^<>]*>)|( +)/$1 || ' '.(' ' x (length($2)-1))/ge;
|
||||
if (!$p)
|
||||
if (!$p && $m[0] ne '' && !$m[2])
|
||||
{
|
||||
$text .= '<p>';
|
||||
$p = 1;
|
||||
|
@ -625,15 +625,15 @@ sub bz_crypt
|
|||
$algorithm = $1;
|
||||
}
|
||||
|
||||
my $crypted_password;
|
||||
if (!$algorithm)
|
||||
{
|
||||
# Wide characters cause crypt 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);
|
||||
|
||||
|
@ -932,7 +932,7 @@ sub xml_dump_simple
|
|||
}
|
||||
elsif ($data =~ 'HASH')
|
||||
{
|
||||
$r = join '', map { xml_element($_, '', xml_dump_simple($data->{$_})) } keys %$data;
|
||||
$r = join '', map { xml_element((/^[a-z:_][a-z:_\.0-9]*$/is ? ($_, '') : ('i', { key => $_ })), xml_dump_simple($data->{$_})) } keys %$data;
|
||||
}
|
||||
elsif ($data =~ 'SCALAR')
|
||||
{
|
||||
|
|
|
@ -72,8 +72,6 @@ sub refresh_some_views
|
|||
my ($userid) = $dbh->selectrow_array('SELECT userid FROM profiles WHERE login_name LIKE ? ORDER BY userid LIMIT 1', undef, $q.'@%');
|
||||
$userid or next;
|
||||
my $userobj = Bugzilla::User->new($userid) or next;
|
||||
# Modify current user (hack)
|
||||
Bugzilla->request_cache->{user} = $userobj;
|
||||
# Determine saved search
|
||||
$q = $query;
|
||||
$q =~ tr/_/%/;
|
||||
|
@ -82,11 +80,11 @@ sub refresh_some_views
|
|||
my $storedquery = Bugzilla::Search::Saved->new({ name => $q, user => $userid }) or next;
|
||||
$storedquery = http_decode_query($storedquery->query);
|
||||
# get SQL code
|
||||
my $search = new Bugzilla::Search(
|
||||
my $search = eval { new Bugzilla::Search(
|
||||
params => $storedquery,
|
||||
fields => [ 'bug_id', grep { $_ ne 'bug_id' } split(/[ ,]+/, $storedquery->{columnlist} || '') ],
|
||||
user => $userobj,
|
||||
) or next;
|
||||
) } or next;
|
||||
# Re-create views
|
||||
my $drop = "DROP VIEW IF EXISTS view\$$user\$$query\$";
|
||||
my $create = "CREATE VIEW view\$$user\$$query\$";
|
||||
|
@ -108,11 +106,9 @@ sub refresh_some_views
|
|||
$dbh->do($drop.'longdescs');
|
||||
$dbh->do($drop.'bugs_activity');
|
||||
$dbh->do($create.'bugs AS '.$sql);
|
||||
$dbh->do($create.'longdescs AS SELECT l.bug_id, u.login_name, l.bug_when, l.thetext, l.work_time FROM longdescs l INNER JOIN '.$bugids.' b ON b.bug_id=l.bug_id INNER JOIN profiles u ON u.userid=l.who'.($userobj->is_insider?'':' WHERE l.isprivate=0'));
|
||||
$dbh->do($create.'bugs_activity AS SELECT a.bug_id, u.login_name, a.bug_when, f.name field_name, a.removed, a.added FROM bugs_activity a INNER JOIN '.$bugids.' b ON b.bug_id=a.bug_id INNER JOIN profiles u ON u.userid=a.who INNER JOIN fielddefs f ON f.id=a.fieldid');
|
||||
$dbh->do($create.'longdescs AS SELECT l.comment_id, l.bug_id, u.login_name, l.bug_when, l.thetext, l.work_time FROM longdescs l INNER JOIN '.$bugids.' b ON b.bug_id=l.bug_id INNER JOIN profiles u ON u.userid=l.who'.($userobj->is_insider?'':' WHERE l.isprivate=0'));
|
||||
$dbh->do($create.'bugs_activity AS SELECT a.id, a.bug_id, u.login_name, a.bug_when, f.name field_name, a.removed, a.added FROM bugs_activity a INNER JOIN '.$bugids.' b ON b.bug_id=a.bug_id INNER JOIN profiles u ON u.userid=a.who INNER JOIN fielddefs f ON f.id=a.fieldid');
|
||||
}
|
||||
# Restore current user
|
||||
Bugzilla->request_cache->{user} = $old_user;
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
|
@ -45,6 +45,12 @@ sub type {
|
|||
if ($type eq 'dateTime') {
|
||||
$value = $self->datetime_format_outbound($value);
|
||||
}
|
||||
elsif ($type eq 'email') {
|
||||
$type = 'string';
|
||||
if (Bugzilla->params->{'webservice_email_filter'}) {
|
||||
$value = email_filter($value);
|
||||
}
|
||||
}
|
||||
return XMLRPC::Data->type($type)->value($value);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,43 +1,90 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Mads Bondo Dydensborg <mbd@dbc.dk>
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Bugzilla;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw(Bugzilla::WebService);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Util qw(datetime_from);
|
||||
use Bugzilla::WebService::Util qw(validate filter_wants);
|
||||
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 READ_ONLY => qw(
|
||||
extensions
|
||||
parameters
|
||||
timezone
|
||||
time
|
||||
version
|
||||
);
|
||||
|
||||
use constant PUBLIC_METHODS => qw(
|
||||
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
|
||||
);
|
||||
|
||||
# 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
|
||||
);
|
||||
|
||||
sub version {
|
||||
my $self = shift;
|
||||
return { version => $self->type('string', BUGZILLA_VERSION) };
|
||||
|
@ -74,6 +121,32 @@ sub time {
|
|||
};
|
||||
}
|
||||
|
||||
sub last_audit_time {
|
||||
my ($self, $params) = validate(@_, 'class');
|
||||
return {
|
||||
last_audit_time => undef
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -91,9 +164,11 @@ This provides functions that tell you about Bugzilla in general.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
=over
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=item C<version>
|
||||
=head2 version
|
||||
|
||||
B<STABLE>
|
||||
|
||||
|
@ -103,6 +178,12 @@ B<STABLE>
|
|||
|
||||
Returns the current version of Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/version
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -112,9 +193,17 @@ string.
|
|||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=item C<extensions>
|
||||
=back
|
||||
|
||||
=head2 extensions
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
|
@ -125,6 +214,12 @@ B<EXPERIMENTAL>
|
|||
Gets information about the extensions that are currently installed and enabled
|
||||
in this Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/extensions
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -155,11 +250,13 @@ The return value looks something like this:
|
|||
that the extensions define themselves. Before 3.6, the names of the
|
||||
extensions depended on the directory they were in on the Bugzilla server.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item C<timezone>
|
||||
=head2 timezone
|
||||
|
||||
B<DEPRECATED> This method may be removed in a future version of Bugzilla.
|
||||
Use L</time> instead.
|
||||
|
@ -170,6 +267,12 @@ Use L</time> instead.
|
|||
|
||||
Returns the timezone that Bugzilla expects dates and times in.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/timezone
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -184,12 +287,14 @@ string in (+/-)XXXX (RFC 2822) format.
|
|||
=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000>
|
||||
(the UTC timezone).
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=item C<time>
|
||||
=head2 time
|
||||
|
||||
B<STABLE>
|
||||
|
||||
|
@ -200,6 +305,12 @@ B<STABLE>
|
|||
Gets information about what time the Bugzilla server thinks it is, and
|
||||
what timezone it's running in.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/time
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -260,9 +371,136 @@ with versions of Bugzilla before 3.6.)
|
|||
were in the UTC timezone, instead of returning information in the server's
|
||||
local timezone.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 parameters
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Returns parameter values currently used in this Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/parameters
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params> (none)
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with a single item C<parameters> which contains a hash with
|
||||
the name of the parameters as keys and their value as values. All
|
||||
values are returned as strings.
|
||||
The list of parameters returned by this method depends on the user
|
||||
credentials:
|
||||
|
||||
A logged-out user can only access the C<maintainer> and C<requirelogin> parameters.
|
||||
|
||||
A logged-in user can access the following parameters (listed alphabetically):
|
||||
C<allowemailchange>,
|
||||
C<attachment_base>,
|
||||
C<commentonchange_resolution>,
|
||||
C<commentonduplicate>,
|
||||
C<cookiepath>,
|
||||
C<defaultopsys>,
|
||||
C<defaultplatform>,
|
||||
C<defaultpriority>,
|
||||
C<defaultseverity>,
|
||||
C<duplicate_or_move_bug_status>,
|
||||
C<emailregexpdesc>,
|
||||
C<emailsuffix>,
|
||||
C<letsubmitterchoosemilestone>,
|
||||
C<letsubmitterchoosepriority>,
|
||||
C<mailfrom>,
|
||||
C<maintainer>,
|
||||
C<maxattachmentsize>,
|
||||
C<maxlocalattachment>,
|
||||
C<musthavemilestoneonaccept>,
|
||||
C<noresolveonopenblockers>,
|
||||
C<password_complexity>,
|
||||
C<rememberlogin>,
|
||||
C<requirelogin>,
|
||||
C<search_allow_no_criteria>,
|
||||
C<urlbase>,
|
||||
C<use_see_also>,
|
||||
C<useclassification>,
|
||||
C<usemenuforusers>,
|
||||
C<useqacontact>,
|
||||
C<usestatuswhiteboard>,
|
||||
C<usetargetmilestone>.
|
||||
|
||||
A user in the tweakparams group can access all existing parameters.
|
||||
New parameters can appear or obsolete parameters can disappear depending
|
||||
on the version of Bugzilla and on extensions being installed.
|
||||
The list of parameters returned by this method is not stable and will
|
||||
never be stable.
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 last_audit_time
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Gets the latest time of the audit_log table.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
GET /rest/last_audit_time
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
You can pass the optional parameter C<class> to get the maximum for only
|
||||
the listed classes.
|
||||
|
||||
=over
|
||||
|
||||
=item C<class> (array) - An array of strings representing the class names.
|
||||
|
||||
B<Note:> The class names are defined as "Bugzilla::<class_name>". For the product
|
||||
use Bugzilla:Product.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with a single item, C<last_audit_time>, that is the maximum of the
|
||||
at_time from the audit_log.
|
||||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Classification;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw (Bugzilla::WebService);
|
||||
|
||||
use Bugzilla::Classification;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::WebService::Util qw(filter validate params_to_objects);
|
||||
|
||||
use constant READ_ONLY => qw(
|
||||
get
|
||||
);
|
||||
|
||||
use constant PUBLIC_METHODS => qw(
|
||||
get
|
||||
);
|
||||
|
||||
sub get {
|
||||
my ($self, $params) = validate(@_, 'names', 'ids');
|
||||
|
||||
defined $params->{names} || defined $params->{ids}
|
||||
|| ThrowCodeError('params_required', { function => 'Classification.get',
|
||||
params => ['names', 'ids'] });
|
||||
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
Bugzilla->params->{'useclassification'}
|
||||
|| $user->in_group('editclassifications')
|
||||
|| ThrowUserError('auth_classification_not_enabled');
|
||||
|
||||
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 @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
|
||||
|
||||
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 ],
|
||||
};
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Classification - The Classification API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla API allows you to deal with the available Classifications.
|
||||
You will be able to get information about them as well as manipulate them.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Classification Retrieval
|
||||
|
||||
=head2 get
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Returns a hash containing information about a set of classifications.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To return information on a single classification:
|
||||
|
||||
GET /rest/classification/<classification_id_or_name>
|
||||
|
||||
The returned data format will be the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
In addition to the parameters below, this method also accepts the
|
||||
standard L<include_fields|Bugzilla::WebService/include_fields> and
|
||||
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
|
||||
|
||||
You could get classifications info by supplying their names and/or ids.
|
||||
So, this method accepts the following parameters:
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
An array of classification ids.
|
||||
|
||||
=item C<names>
|
||||
|
||||
An array of classification names.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with the key C<classifications> and an array of hashes as the corresponding value.
|
||||
Each element of the array represents a classification that the user is authorized to see
|
||||
and has the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the classification.
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The name of the classification.
|
||||
|
||||
=item C<description>
|
||||
|
||||
C<string> The description of the classificaion.
|
||||
|
||||
=item C<sort_key>
|
||||
|
||||
C<int> The value which determines the order the classification is sorted.
|
||||
|
||||
=item C<products>
|
||||
|
||||
An array of hashes. The array contains the products the user is authorized to
|
||||
access within the classification. Each hash has the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The name of the product.
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the product.
|
||||
|
||||
=item C<description>
|
||||
|
||||
C<string> The description of the product.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 900 (Classification not enabled)
|
||||
|
||||
Classification is not enabled on this installation.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Component;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw(Bugzilla::WebService);
|
||||
|
||||
use Bugzilla::Component;
|
||||
use Bugzilla::Constants;
|
||||
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 MAPPED_FIELDS => {
|
||||
default_assignee => 'initialowner',
|
||||
default_qa_contact => 'initialqacontact',
|
||||
default_cc => 'initial_cc',
|
||||
is_open => 'isactive',
|
||||
};
|
||||
|
||||
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 => 'edit',
|
||||
object => 'components' });
|
||||
|
||||
my $product = $user->check_can_admin_product($params->{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) };
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Component - The Component API
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla API allows you to deal with the available product components.
|
||||
You will be able to get information about them as well as manipulate them.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
=head1 Component Creation and Modification
|
||||
|
||||
=head2 create
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
This allows you to create a new component in Bugzilla.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
Some params must be set, or an error will be thrown. These params are
|
||||
marked B<Required>.
|
||||
|
||||
=over
|
||||
|
||||
=item C<name>
|
||||
|
||||
B<Required> C<string> The name of the new component.
|
||||
|
||||
=item C<product>
|
||||
|
||||
B<Required> C<string> The name of the product that the component must be
|
||||
added to. This product must already exist, and the user have the necessary
|
||||
permissions to edit components for it.
|
||||
|
||||
=item C<description>
|
||||
|
||||
B<Required> C<string> The description of the new component.
|
||||
|
||||
=item C<default_assignee>
|
||||
|
||||
B<Required> C<string> The login name of the default assignee of the component.
|
||||
|
||||
=item C<default_cc>
|
||||
|
||||
C<array> An array of strings with each element representing one login name of the default CC list.
|
||||
|
||||
=item C<default_qa_contact>
|
||||
|
||||
C<string> The login name of the default QA contact for the component.
|
||||
|
||||
=item C<is_open>
|
||||
|
||||
C<boolean> 1 if you want to enable the component for bug creations. 0 otherwise. Default is 1.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with one key: C<id>. This will represent the ID of the newly-added
|
||||
component.
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 304 (Authorization Failure)
|
||||
|
||||
You are not authorized to create a new component.
|
||||
|
||||
=item 1200 (Component already exists)
|
||||
|
||||
The name that you specified for the new component already exists in the
|
||||
specified product.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
|
@ -1,30 +1,37 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Constants;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw(Exporter);
|
||||
|
||||
our @EXPORT = qw(
|
||||
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
|
||||
|
||||
ERROR_UNKNOWN_FATAL
|
||||
ERROR_UNKNOWN_TRANSIENT
|
||||
|
||||
XMLRPC_CONTENT_TYPE_WHITELIST
|
||||
REST_CONTENT_TYPE_WHITELIST
|
||||
|
||||
WS_DISPATCH
|
||||
);
|
||||
|
||||
|
@ -49,13 +56,17 @@ our @EXPORT = qw(
|
|||
use constant WS_ERROR_CODE => {
|
||||
# Generic errors (Bugzilla::Object and others) are 50-99.
|
||||
object_not_specified => 50,
|
||||
reassign_to_empty => 50,
|
||||
param_required => 50,
|
||||
params_required => 50,
|
||||
undefined_field => 50,
|
||||
object_does_not_exist => 51,
|
||||
param_must_be_numeric => 52,
|
||||
number_not_numeric => 52,
|
||||
param_invalid => 53,
|
||||
number_too_large => 54,
|
||||
number_too_small => 55,
|
||||
illegal_date => 56,
|
||||
# Bug errors usually occupy the 100-200 range.
|
||||
improper_bug_id_field_value => 100,
|
||||
bug_id_does_not_exist => 101,
|
||||
|
@ -66,12 +77,14 @@ use constant WS_ERROR_CODE => {
|
|||
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,
|
||||
|
@ -87,6 +100,12 @@ use constant WS_ERROR_CODE => {
|
|||
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,
|
||||
|
@ -95,14 +114,39 @@ use constant WS_ERROR_CODE => {
|
|||
# Note: 114 is above in the Comment-related section.
|
||||
# Bug update errors
|
||||
illegal_change => 115,
|
||||
# Dependency errors
|
||||
dependency_loop_single => 116,
|
||||
dependency_loop_multi => 116,
|
||||
# Note: 117 is above in the Comment-related section.
|
||||
# Dup errors
|
||||
dupe_loop_detected => 118,
|
||||
dupe_id_required => 119,
|
||||
# Bug-related group errors
|
||||
group_invalid_removal => 120,
|
||||
group_restriction_not_allowed => 120,
|
||||
# Status/Resolution errors
|
||||
missing_resolution => 121,
|
||||
resolution_not_allowed => 122,
|
||||
illegal_bug_status_transition => 123,
|
||||
# Flag errors
|
||||
flag_status_invalid => 129,
|
||||
flag_update_denied => 130,
|
||||
flag_type_requestee_disabled => 131,
|
||||
flag_not_unique => 132,
|
||||
flag_type_not_unique => 133,
|
||||
flag_type_inactive => 134,
|
||||
|
||||
# Authentication errors are usually 300-400.
|
||||
invalid_username_or_password => 300,
|
||||
invalid_login_or_password => 300,
|
||||
account_disabled => 301,
|
||||
auth_invalid_email => 302,
|
||||
extern_id_conflict => -303,
|
||||
auth_failure => 304,
|
||||
password_current_too_short => 305,
|
||||
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,
|
||||
|
@ -122,10 +166,105 @@ use constant WS_ERROR_CODE => {
|
|||
user_access_by_id_denied => 505,
|
||||
user_access_by_match_denied => 505,
|
||||
|
||||
# RPC Server Errors. See the following URL:
|
||||
# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
|
||||
# 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
|
||||
# to describe whether a call was successful or
|
||||
# to describe the type of error that occurred.
|
||||
use constant STATUS_OK => 200;
|
||||
use constant STATUS_CREATED => 201;
|
||||
use constant STATUS_ACCEPTED => 202;
|
||||
use constant STATUS_NO_CONTENT => 204;
|
||||
use constant STATUS_MULTIPLE_CHOICES => 300;
|
||||
use constant STATUS_BAD_REQUEST => 400;
|
||||
use constant STATUS_NOT_AUTHORIZED => 401;
|
||||
use constant STATUS_NOT_FOUND => 404;
|
||||
use constant STATUS_GONE => 410;
|
||||
|
||||
# The integer value is the error code above returned by
|
||||
# the related webvservice call. We choose the appropriate
|
||||
# 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;
|
||||
};
|
||||
|
||||
# These are the fallback defaults for errors not in ERROR_CODE.
|
||||
|
@ -134,6 +273,19 @@ use constant ERROR_UNKNOWN_TRANSIENT => 32000;
|
|||
|
||||
use constant ERROR_GENERAL => 999;
|
||||
|
||||
use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
|
||||
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
|
||||
);
|
||||
|
||||
sub WS_DISPATCH {
|
||||
# We "require" here instead of "use" above to avoid a dependency loop.
|
||||
require Bugzilla::Hook;
|
||||
|
@ -143,8 +295,12 @@ sub WS_DISPATCH {
|
|||
my $dispatch = {
|
||||
'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
|
||||
'Bug' => 'Bugzilla::WebService::Bug',
|
||||
'User' => 'Bugzilla::WebService::User',
|
||||
'Classification' => 'Bugzilla::WebService::Classification',
|
||||
'Component' => 'Bugzilla::WebService::Component',
|
||||
'FlagType' => 'Bugzilla::WebService::FlagType',
|
||||
'Group' => 'Bugzilla::WebService::Group',
|
||||
'Product' => 'Bugzilla::WebService::Product',
|
||||
'User' => 'Bugzilla::WebService::User',
|
||||
'Field' => 'Bugzilla::WebService::Field',
|
||||
%hook_dispatch
|
||||
};
|
||||
|
@ -152,3 +308,13 @@ sub WS_DISPATCH {
|
|||
};
|
||||
|
||||
1;
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item REST_STATUS_CODE_MAP
|
||||
|
||||
=item WS_DISPATCH
|
||||
|
||||
=back
|
||||
|
|
|
@ -0,0 +1,833 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::FlagType;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw(Bugzilla::WebService);
|
||||
use Bugzilla::Component;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::FlagType;
|
||||
use Bugzilla::Product;
|
||||
use Bugzilla::Util qw(trim);
|
||||
|
||||
use List::MoreUtils qw(uniq);
|
||||
|
||||
use constant PUBLIC_METHODS => qw(
|
||||
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;
|
||||
}
|
||||
|
||||
sub create {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
Bugzilla->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};
|
||||
|
||||
my $flagtype = Bugzilla::FlagType->create(\%args);
|
||||
|
||||
return { id => $self->type('int', $flagtype->id) };
|
||||
}
|
||||
|
||||
sub update {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $user = Bugzilla->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 });
|
||||
}
|
||||
|
||||
# Process the clusions
|
||||
foreach my $type ('inclusions', 'exclusions') {
|
||||
my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
|
||||
next if not defined $clusions;
|
||||
|
||||
my @extra_clusions = ();
|
||||
if (!$user->in_group('editcomponents')) {
|
||||
my $products = $user->get_products_by_permission('editcomponents');
|
||||
# Bring back the products the user cannot edit.
|
||||
foreach my $item (values %{$flagtype->$type}) {
|
||||
my ($prod_id, $comp_id) = split(':', $item);
|
||||
push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products;
|
||||
}
|
||||
}
|
||||
|
||||
$flagtype->set_clusions({
|
||||
$type => [@$clusions, @extra_clusions],
|
||||
});
|
||||
}
|
||||
|
||||
my $returned_changes = $flagtype->update();
|
||||
$changes{$flagtype->id} = {
|
||||
name => $flagtype->name,
|
||||
changes => $returned_changes,
|
||||
};
|
||||
}
|
||||
$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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
push @$result, $comp_id;
|
||||
}
|
||||
else {
|
||||
return [ '' ];
|
||||
}
|
||||
}
|
||||
}
|
||||
$result = Bugzilla::Component->match({ product_id => $product_id, name => $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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::WebService::FlagType - API for creating flags.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla API allows you to create new flags
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
See L<Bugzilla::WebService> for a description of what B<STABLE>, B<UNSTABLE>,
|
||||
and B<EXPERIMENTAL> mean, and for more description about error codes.
|
||||
|
||||
=head2 Get Flag Types
|
||||
|
||||
=over
|
||||
|
||||
=item C<get> B<UNSTABLE>
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Get information about valid flag types that can be set for bugs and attachments.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
You have several options for retreiving information about flag types. The first
|
||||
part is the request method and the rest is the related path needed.
|
||||
|
||||
To get information about all flag types for a product:
|
||||
|
||||
GET /rest/flag_type/<product>
|
||||
|
||||
To get information about flag_types for a product and component:
|
||||
|
||||
GET /rest/flag_type/<product>/<component>
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
You must pass a product name and an optional component name.
|
||||
|
||||
=over
|
||||
|
||||
=item C<product> (string) - The name of a valid product.
|
||||
|
||||
=item C<component> (string) - An optional valid component name associated with the product.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes,
|
||||
containing the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> An integer id uniquely identifying this flag type.
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The name for the flag type.
|
||||
|
||||
=item C<type>
|
||||
|
||||
C<string> The target of the flag type which is either C<bug> or C<attachment>.
|
||||
|
||||
=item C<description>
|
||||
|
||||
C<string> The description of the flag type.
|
||||
|
||||
=item C<values>
|
||||
|
||||
C<array> An array of string values that the user can set on the flag type.
|
||||
|
||||
=item C<is_requesteeble>
|
||||
|
||||
C<boolean> Users can ask specific other users to set flags of this type.
|
||||
|
||||
=item C<is_multiplicable>
|
||||
|
||||
C<boolean> Multiple flags of this type can be set for the same bug or attachment.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 106 (Product Access Denied)
|
||||
|
||||
Either the product does not exist or you don't have access to it.
|
||||
|
||||
=item 51 (Invalid Component)
|
||||
|
||||
The component provided does not exist in the product.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 Create Flag
|
||||
|
||||
=over
|
||||
|
||||
=item C<create> B<UNSTABLE>
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Creates a new FlagType
|
||||
|
||||
=item B<REST>
|
||||
|
||||
POST /rest/flag_type
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
At a minimum the following two arguments must be supplied:
|
||||
|
||||
=over
|
||||
|
||||
=item C<name> (string) - The name of the new Flag Type.
|
||||
|
||||
=item C<description> (string) - A description for the Flag Type object.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
C<int> flag_id
|
||||
|
||||
The ID of the new FlagType object is returned.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item name B<required>
|
||||
|
||||
C<string> A short name identifying this type.
|
||||
|
||||
=item description B<required>
|
||||
|
||||
C<string> A comprehensive description of this type.
|
||||
|
||||
=item inclusions B<optional>
|
||||
|
||||
An array of strings or a hash containing product names, and optionally
|
||||
component names. If you provide a string, the flag type will be shown on
|
||||
all bugs in that product. If you provide a hash, the key represents the
|
||||
product name, and the value is the components of the product to be included.
|
||||
|
||||
For example:
|
||||
|
||||
[ 'FooProduct',
|
||||
{
|
||||
BarProduct => [ 'C1', 'C3' ],
|
||||
BazProduct => [ 'C7' ]
|
||||
}
|
||||
]
|
||||
|
||||
This flag will be added to B<All> components of I<FooProduct>,
|
||||
components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
|
||||
|
||||
=item exclusions B<optional>
|
||||
|
||||
An array of strings or hashes containing product names. This uses the same
|
||||
fromat as inclusions.
|
||||
|
||||
This will exclude the flag from all products and components specified.
|
||||
|
||||
=item sortkey B<optional>
|
||||
|
||||
C<int> A number between 1 and 32767 by which this type will be sorted when
|
||||
displayed to users in a list; ignore if you don't care what order the types
|
||||
appear in or if you want them to appear in alphabetical order.
|
||||
|
||||
=item is_active B<optional>
|
||||
|
||||
C<boolean> Flag of this type appear in the UI and can be set. Default is B<true>.
|
||||
|
||||
=item is_requestable B<optional>
|
||||
|
||||
C<boolean> Users can ask for flags of this type to be set. Default is B<true>.
|
||||
|
||||
=item cc_list B<optional>
|
||||
|
||||
C<array> An array of strings. If the flag type is requestable, who should
|
||||
receive e-mail notification of requests. This is an array of e-mail addresses
|
||||
which do not need to be Bugzilla logins.
|
||||
|
||||
=item is_specifically_requestable B<optional>
|
||||
|
||||
C<boolean> Users can ask specific other users to set flags of this type as
|
||||
opposed to just asking the wind. Default is B<true>.
|
||||
|
||||
=item is_multiplicable B<optional>
|
||||
|
||||
C<boolean> Multiple flags of this type can be set on the same bug. Default is B<true>.
|
||||
|
||||
=item grant_group B<optional>
|
||||
|
||||
C<string> The group allowed to grant/deny flags of this type (to allow all
|
||||
users to grant/deny these flags, select no group). Default is B<no group>.
|
||||
|
||||
=item request_group B<optional>
|
||||
|
||||
C<string> If flags of this type are requestable, the group allowed to request
|
||||
them (to allow all users to request these flags, select no group). Note that
|
||||
the request group alone has no effect if the grant group is not defined!
|
||||
Default is B<no group>.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 51 (Group Does Not Exist)
|
||||
|
||||
The group name you entered does not exist, or you do not have access to it.
|
||||
|
||||
=item 105 (Unknown component)
|
||||
|
||||
The component does not exist for this product.
|
||||
|
||||
=item 106 (Product Access Denied)
|
||||
|
||||
Either the product does not exist or you don't have editcomponents privileges
|
||||
to it.
|
||||
|
||||
=item 501 (Illegal Email Address)
|
||||
|
||||
One of the e-mail address in the CC list is invalid. An e-mail in the CC
|
||||
list does NOT need to be a valid Bugzilla user.
|
||||
|
||||
=item 1101 (Flag Type Name invalid)
|
||||
|
||||
You must specify a non-blank name for this flag type. It must
|
||||
no contain spaces or commas, and must be 50 characters or less.
|
||||
|
||||
=item 1102 (Flag type must have description)
|
||||
|
||||
You must specify a description for this flag type.
|
||||
|
||||
=item 1103 (Flag type CC list is invalid
|
||||
|
||||
The CC list must be 200 characters or less.
|
||||
|
||||
=item 1104 (Flag Type Sort Key Not Valid)
|
||||
|
||||
The sort key is not a valid number.
|
||||
|
||||
=item 1105 (Flag Type Not Editable)
|
||||
|
||||
This flag type is not available for the products you can administer. Therefore
|
||||
you can not edit attributes of the flag type, other than the inclusion and
|
||||
exclusion list.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 update
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
This allows you to update a flag type in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /rest/flag_type/<product_id_or_name>
|
||||
|
||||
The params to include in the PUT body as well as the returned data format,
|
||||
are the same as below. The C<ids> and C<names> params will be overridden as
|
||||
it is pulled from the URL path.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note:> The following parameters specify which products you are updating.
|
||||
You must set one or both of these parameters.
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
C<array> of C<int>s. Numeric ids of the flag types that you wish to update.
|
||||
|
||||
=item C<names>
|
||||
|
||||
C<array> of C<string>s. Names of the flag types that you wish to update. If
|
||||
many flag types have the same name, this will change ALL of them.
|
||||
|
||||
=back
|
||||
|
||||
B<Note:> The following parameters specify the new values you want to set for
|
||||
the products you are updating.
|
||||
|
||||
=over
|
||||
|
||||
=item name
|
||||
|
||||
C<string> A short name identifying this type.
|
||||
|
||||
=item description
|
||||
|
||||
C<string> A comprehensive description of this type.
|
||||
|
||||
=item inclusions B<optional>
|
||||
|
||||
An array of strings or a hash containing product names, and optionally
|
||||
component names. If you provide a string, the flag type will be shown on
|
||||
all bugs in that product. If you provide a hash, the key represents the
|
||||
product name, and the value is the components of the product to be included.
|
||||
|
||||
for example
|
||||
|
||||
[ 'FooProduct',
|
||||
{
|
||||
BarProduct => [ 'C1', 'C3' ],
|
||||
BazProduct => [ 'C7' ]
|
||||
}
|
||||
]
|
||||
|
||||
This flag will be added to B<All> components of I<FooProduct>,
|
||||
components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
|
||||
|
||||
=item exclusions B<optional>
|
||||
|
||||
An array of strings or hashes containing product names.
|
||||
This uses the same fromat as inclusions.
|
||||
|
||||
This will exclude the flag from all products and components specified.
|
||||
|
||||
=item sortkey
|
||||
|
||||
C<int> A number between 1 and 32767 by which this type will be sorted when
|
||||
displayed to users in a list; ignore if you don't care what order the types
|
||||
appear in or if you want them to appear in alphabetical order.
|
||||
|
||||
=item is_active
|
||||
|
||||
C<boolean> Flag of this type appear in the UI and can be set.
|
||||
|
||||
=item is_requestable
|
||||
|
||||
C<boolean> Users can ask for flags of this type to be set.
|
||||
|
||||
=item cc_list
|
||||
|
||||
C<array> An array of strings. If the flag type is requestable, who should
|
||||
receive e-mail notification of requests. This is an array of e-mail addresses
|
||||
which do not need to be Bugzilla logins.
|
||||
|
||||
=item is_specifically_requestable
|
||||
|
||||
C<boolean> Users can ask specific other users to set flags of this type as
|
||||
opposed to just asking the wind.
|
||||
|
||||
=item is_multiplicable
|
||||
|
||||
C<boolean> Multiple flags of this type can be set on the same bug.
|
||||
|
||||
=item grant_group
|
||||
|
||||
C<string> The group allowed to grant/deny flags of this type (to allow all
|
||||
users to grant/deny these flags, select no group).
|
||||
|
||||
=item request_group
|
||||
|
||||
C<string> If flags of this type are requestable, the group allowed to request
|
||||
them (to allow all users to request these flags, select no group). Note that
|
||||
the request group alone has no effect if the grant group is not defined!
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A C<hash> with a single field "flagtypes". This points to an array of hashes
|
||||
with the following fields:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the product that was updated.
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> The name of the product that was updated.
|
||||
|
||||
=item C<changes>
|
||||
|
||||
C<hash> The changes that were actually done on this product. The keys are
|
||||
the names of the fields that were changed, and the values are a hash
|
||||
with two keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<added>
|
||||
|
||||
C<string> The value that this field was changed to.
|
||||
|
||||
=item C<removed>
|
||||
|
||||
C<string> The value that was previously set in this field.
|
||||
|
||||
=back
|
||||
|
||||
Note that booleans will be represented with the strings '1' and '0'.
|
||||
|
||||
Here's an example of what a return value might look like:
|
||||
|
||||
{
|
||||
products => [
|
||||
{
|
||||
id => 123,
|
||||
changes => {
|
||||
name => {
|
||||
removed => 'FooFlagType',
|
||||
added => 'BarFlagType'
|
||||
},
|
||||
is_requestable => {
|
||||
removed => '1',
|
||||
added => '0',
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 51 (Group Does Not Exist)
|
||||
|
||||
The group name you entered does not exist, or you do not have access to it.
|
||||
|
||||
=item 105 (Unknown component)
|
||||
|
||||
The component does not exist for this product.
|
||||
|
||||
=item 106 (Product Access Denied)
|
||||
|
||||
Either the product does not exist or you don't have editcomponents privileges
|
||||
to it.
|
||||
|
||||
=item 501 (Illegal Email Address)
|
||||
|
||||
One of the e-mail address in the CC list is invalid. An e-mail in the CC
|
||||
list does NOT need to be a valid Bugzilla user.
|
||||
|
||||
=item 1101 (Flag Type Name invalid)
|
||||
|
||||
You must specify a non-blank name for this flag type. It must
|
||||
no contain spaces or commas, and must be 50 characters or less.
|
||||
|
||||
=item 1102 (Flag type must have description)
|
||||
|
||||
You must specify a description for this flag type.
|
||||
|
||||
=item 1103 (Flag type CC list is invalid
|
||||
|
||||
The CC list must be 200 characters or less.
|
||||
|
||||
=item 1104 (Flag Type Sort Key Not Valid)
|
||||
|
||||
The sort key is not a valid number.
|
||||
|
||||
=item 1105 (Flag Type Not Editable)
|
||||
|
||||
This flag type is not available for the products you can administer. Therefore
|
||||
you can not edit attributes of the flag type, other than the inclusion and
|
||||
exclusion list.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
|
@ -0,0 +1,695 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Group;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base qw(Bugzilla::WebService);
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::WebService::Util qw(validate translate params_to_objects user_to_hash);
|
||||
|
||||
use constant PUBLIC_METHODS => qw(
|
||||
create
|
||||
get
|
||||
update
|
||||
add_members
|
||||
remove_members
|
||||
add_managers
|
||||
remove_managers
|
||||
);
|
||||
|
||||
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) };
|
||||
}
|
||||
|
||||
sub update {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
Bugzilla->login(LOGIN_REQUIRED);
|
||||
Bugzilla->user->in_group('creategroups')
|
||||
|| ThrowUserError("auth_failure", { group => "creategroups",
|
||||
action => "edit",
|
||||
object => "group" });
|
||||
|
||||
defined($params->{names}) || defined($params->{ids})
|
||||
|| ThrowCodeError('params_required',
|
||||
{ function => 'Group.update', params => ['ids', 'names'] });
|
||||
|
||||
my $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);
|
||||
}
|
||||
|
||||
return { groups => \@result };
|
||||
}
|
||||
|
||||
sub get {
|
||||
my ($self, $params) = validate(@_, 'ids', 'names', 'type');
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
my $groups = [];
|
||||
|
||||
if (defined $params->{ids}) {
|
||||
# Get the groups by id
|
||||
$groups = Bugzilla::Group->new_from_list($params->{ids});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
sub add_members
|
||||
{
|
||||
my $self = shift;
|
||||
$self->_update_users(0, 1, @_);
|
||||
}
|
||||
|
||||
sub remove_members
|
||||
{
|
||||
my $self = shift;
|
||||
$self->_update_users(0, 0, @_);
|
||||
}
|
||||
|
||||
sub add_managers
|
||||
{
|
||||
my $self = shift;
|
||||
$self->_update_users(1, 1, @_);
|
||||
}
|
||||
|
||||
sub remove_managers
|
||||
{
|
||||
my $self = shift;
|
||||
$self->_update_users(1, 0, @_);
|
||||
}
|
||||
|
||||
sub _update_users
|
||||
{
|
||||
my ($self, $isbless, $add, $params) = @_;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
defined($params->{names}) || defined($params->{ids})
|
||||
|| ThrowCodeError('params_required',
|
||||
{ function => 'Group.update', params => ['ids', 'names'] });
|
||||
my @group_objects;
|
||||
if ($params->{names})
|
||||
{
|
||||
push @group_objects, @{ Bugzilla::Group->match({ name => $params->{names} }) };
|
||||
}
|
||||
if ($params->{ids})
|
||||
{
|
||||
push @group_objects, @{ Bugzilla::Group->match({ id => $params->{ids} }) };
|
||||
}
|
||||
|
||||
Bugzilla->login(LOGIN_REQUIRED);
|
||||
if ($isbless)
|
||||
{
|
||||
Bugzilla->user->in_group('editusers')
|
||||
|| ThrowUserError("auth_failure", {
|
||||
group => "editusers",
|
||||
action => "edit",
|
||||
object => "group"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Bugzilla->user->in_group('creategroups')
|
||||
|| (!grep { !Bugzilla->user->can_bless($_->id) } @group_objects)
|
||||
|| ThrowUserError("auth_failure", {
|
||||
group => "creategroups",
|
||||
action => "edit",
|
||||
object => "group"
|
||||
});
|
||||
}
|
||||
|
||||
my @user_objects;
|
||||
if ($params->{usernames})
|
||||
{
|
||||
push @user_objects, @{ Bugzilla::User->match({ login_name => $params->{usernames} }) };
|
||||
}
|
||||
if ($params->{userids})
|
||||
{
|
||||
push @user_objects, @{ Bugzilla::User->new_from_list($params->{userids}) };
|
||||
}
|
||||
@user_objects = grep { Bugzilla->user->can_see_user($_) } @user_objects;
|
||||
@user_objects || ThrowCodeError('params_required', { function => 'Group.update', params => ['userids', 'usernames'] });
|
||||
|
||||
$add = $add ? 'add_user_groups' : 'remove_user_groups';
|
||||
Bugzilla::Group->$add(
|
||||
[ map { my $grp = $_; map { { group => $grp, user => $_ } } @user_objects } @group_objects ], $isbless
|
||||
);
|
||||
|
||||
return {
|
||||
groups => [ map { $self->_group_to_hash({}, $_) } @group_objects ],
|
||||
users => [ map { user_to_hash($self, $_, {}) } @user_objects ],
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
sub _get_group_membership {
|
||||
my ($self, $group, $params) = @_;
|
||||
my $user = Bugzilla->user;
|
||||
|
||||
my %users_only;
|
||||
my $dbh = Bugzilla->dbh;
|
||||
my $editusers = $user->in_group('editusers');
|
||||
|
||||
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 (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
|
||||
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;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Bugzilla::Webservice::Group - The API for creating, changing, and getting
|
||||
information about Groups.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This part of the Bugzilla API allows you to create Groups and
|
||||
get information about them.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=head1 Group Creation and Modification
|
||||
|
||||
=head2 create
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
This allows you to create a new group in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
POST /rest/group
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
Some params must be set, or an error will be thrown. These params are
|
||||
marked B<Required>.
|
||||
|
||||
=over
|
||||
|
||||
=item C<name>
|
||||
|
||||
B<Required> C<string> A short name for this group. Must be unique. This
|
||||
is not usually displayed in the user interface, except in a few places.
|
||||
|
||||
=item C<description>
|
||||
|
||||
B<Required> C<string> A human-readable name for this group. Should be
|
||||
relatively short. This is what will normally appear in the UI as the
|
||||
name of the group.
|
||||
|
||||
=item C<user_regexp>
|
||||
|
||||
C<string> A regular expression. Any user whose Bugzilla username matches
|
||||
this regular expression will automatically be granted membership in this group.
|
||||
|
||||
=item C<is_active>
|
||||
|
||||
C<boolean> C<True> if new group can be used for bugs, C<False> if this
|
||||
is a group that will only contain users and no bugs will be restricted
|
||||
to it.
|
||||
|
||||
=item C<icon_url>
|
||||
|
||||
C<string> A URL pointing to a small icon used to identify the group.
|
||||
This icon will show up next to users' names in various parts of Bugzilla
|
||||
if they are in this group.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A hash with one element, C<id>. This is the id of the newly-created group.
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 800 (Empty Group Name)
|
||||
|
||||
You must specify a value for the C<name> field.
|
||||
|
||||
=item 801 (Group Exists)
|
||||
|
||||
There is already another group with the same C<name>.
|
||||
|
||||
=item 802 (Group Missing Description)
|
||||
|
||||
You must specify a value for the C<description> field.
|
||||
|
||||
=item 803 (Group Regexp Invalid)
|
||||
|
||||
You specified an invalid regular expression in the C<user_regexp> field.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 update
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
This allows you to update a group in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /rest/group/<group_name_or_id>
|
||||
|
||||
The params to include in the PUT body as well as the returned data format,
|
||||
are the same as below. The C<ids> param will be overridden as it is pulled
|
||||
from the URL path.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
At least C<ids> or C<names> must be set, or an error will be thrown.
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
B<Required> C<array> Contain ids of groups to update.
|
||||
|
||||
=item C<names>
|
||||
|
||||
B<Required> C<array> Contain names of groups to update.
|
||||
|
||||
=item C<name>
|
||||
|
||||
C<string> A new name for group.
|
||||
|
||||
=item C<description>
|
||||
|
||||
C<string> A new description for groups. This is what will appear in the UI
|
||||
as the name of the groups.
|
||||
|
||||
=item C<user_regexp>
|
||||
|
||||
C<string> A new regular expression for email. Will automatically grant
|
||||
membership to these groups to anyone with an email address that matches
|
||||
this perl regular expression.
|
||||
|
||||
=item C<is_active>
|
||||
|
||||
C<boolean> Set if groups are active and eligible to be used for bugs.
|
||||
True if bugs can be restricted to this group, false otherwise.
|
||||
|
||||
=item C<icon_url>
|
||||
|
||||
C<string> A URL pointing to an icon that will appear next to the name of
|
||||
users who are in this group.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A C<hash> with a single field "groups". This points to an array of hashes
|
||||
with the following fields:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the group that was updated.
|
||||
|
||||
=item C<changes>
|
||||
|
||||
C<hash> The changes that were actually done on this group. The keys are
|
||||
the names of the fields that were changed, and the values are a hash
|
||||
with two keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<added>
|
||||
|
||||
C<string> The values that were added to this field,
|
||||
possibly a comma-and-space-separated list if multiple values were added.
|
||||
|
||||
=item C<removed>
|
||||
|
||||
C<string> The values that were removed from this field, possibly a
|
||||
comma-and-space-separated list if multiple values were removed.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
The same as L</create>.
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 Group Information
|
||||
|
||||
=head2 get
|
||||
|
||||
B<UNSTABLE>
|
||||
|
||||
=over
|
||||
|
||||
=item B<Description>
|
||||
|
||||
Returns information about L<Bugzilla::Group|Groups>.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To return information about a specific group by C<id> or C<name>:
|
||||
|
||||
GET /rest/group/<group_id_or_name>
|
||||
|
||||
You can also return information about more than one specific group
|
||||
by using the following in your query string:
|
||||
|
||||
GET /rest/group?ids=1&ids=2&ids=3 or GET /group?names=ProductOne&names=Product2
|
||||
|
||||
the returned data format is same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
If neither ids or names is passed, and you are in the creategroups or
|
||||
editusers group, then all groups will be retrieved. Otherwise, only groups
|
||||
that you have bless privileges for will be returned.
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
C<array> Contain ids of groups to update.
|
||||
|
||||
=item C<names>
|
||||
|
||||
C<array> Contain names of groups to update.
|
||||
|
||||
=item C<membership>
|
||||
|
||||
C<boolean> Set to 1 then a list of members of the passed groups' names and
|
||||
ids will be returned.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
If the user is a member of the "creategroups" group they will receive
|
||||
information about all groups or groups matching the criteria that they passed.
|
||||
You have to be in the creategroups group unless you're requesting membership
|
||||
information.
|
||||
|
||||
If the user is not a member of the "creategroups" group, but they are in the
|
||||
"editusers" group or have bless privileges to the groups they require
|
||||
membership information for, the is_active, is_bug_group and user_regexp values
|
||||
are not supplied.
|
||||
|
||||
The return value will be a hash containing group names as the keys, each group
|
||||
name will point to a hash that describes the group and has the following items:
|
||||
|
||||
=over
|
||||
|
||||
=item id
|
||||
|
||||
C<int> The unique integer ID that Bugzilla uses to identify this group.
|
||||
Even if the name of the group changes, this ID will stay the same.
|
||||
|
||||
=item name
|
||||
|
||||
C<string> The name of the group.
|
||||
|
||||
=item description
|
||||
|
||||
C<string> The description of the group.
|
||||
|
||||
=item is_bug_group
|
||||
|
||||
C<int> Whether this groups is to be used for bug reports or is only administrative specific.
|
||||
|
||||
=item user_regexp
|
||||
|
||||
C<string> A regular expression that allows users to be added to this group if their login matches.
|
||||
|
||||
=item is_active
|
||||
|
||||
C<int> Whether this group is currently active or not.
|
||||
|
||||
=item users
|
||||
|
||||
C<array> An array of hashes, each hash contains a user object for one of the
|
||||
members of this group, only returned if the user sets the C<membership>
|
||||
parameter to 1, the user hash has the following items:
|
||||
|
||||
=over
|
||||
|
||||
=item id
|
||||
|
||||
C<int> The id of the user.
|
||||
|
||||
=item real_name
|
||||
|
||||
C<string> The actual name of the user.
|
||||
|
||||
=item email
|
||||
|
||||
C<string> The email address of the user.
|
||||
|
||||
=item name
|
||||
|
||||
C<string> The login name of the user. Note that in some situations this is
|
||||
different than their email.
|
||||
|
||||
=item can_login
|
||||
|
||||
C<boolean> A boolean value to indicate if the user can login into bugzilla.
|
||||
|
||||
=item email_enabled
|
||||
|
||||
C<boolean> A boolean value to indicate if bug-related mail will be sent
|
||||
to the user or not.
|
||||
|
||||
=item disabled_text
|
||||
|
||||
C<string> A text field that holds the reason for disabling a user from logging
|
||||
into bugzilla, if empty then the user account is enabled otherwise it is
|
||||
disabled/closed.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 51 (Invalid Object)
|
||||
|
||||
A non existing group name was passed to the function, as a result no
|
||||
group object existed for that invalid name.
|
||||
|
||||
=item 805 (Cannot view groups)
|
||||
|
||||
Logged-in users are not authorized to edit bugzilla groups as they are not
|
||||
members of the creategroups group in bugzilla, or they are not authorized to
|
||||
access group member's information as they are not members of the "editusers"
|
||||
group or can bless the group.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item This function was added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
File diff suppressed because it is too large
Load Diff
|
@ -1,62 +1,45 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
|
||||
#
|
||||
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Server::XMLRPC;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use SOAP::Transport::HTTP;
|
||||
use warnings;
|
||||
|
||||
use XMLRPC::Transport::HTTP;
|
||||
use Bugzilla::WebService::Server;
|
||||
|
||||
our @ISA = qw(Bugzilla::WebService::Server);
|
||||
push @ISA, 'XMLRPC::Transport::HTTP::' . ($ENV{MOD_PERL} ? 'Apache' : 'CGI');
|
||||
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);
|
||||
}
|
||||
|
||||
use Bugzilla::WebService::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util;
|
||||
|
||||
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;
|
||||
use List::MoreUtils qw(none);
|
||||
|
||||
sub type {
|
||||
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;
|
||||
}
|
||||
|
||||
sub make_response {
|
||||
my $self = shift;
|
||||
|
||||
$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 (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
|
||||
$self->response->headers->push_header('Set-Cookie', $_);
|
||||
elsif ($type eq 'email') {
|
||||
$type = 'string';
|
||||
if (Bugzilla->params->{'webservice_email_filter'}) {
|
||||
$value = email_filter($value);
|
||||
}
|
||||
}
|
||||
|
||||
sub handle_login {
|
||||
my ($self, $classes, $action, $uri, $method) = @_;
|
||||
my $class = $classes->{$uri};
|
||||
my $full_method = $uri . "." . $method;
|
||||
$self->SUPER::handle_login($class, $method, $full_method);
|
||||
return;
|
||||
return XMLRPC::Data->type($type)->value($value);
|
||||
}
|
||||
|
||||
# Patch SOAP::Transport::HTTP::CGI so it works under CGI like HTTP::Server::Simple
|
||||
|
@ -93,21 +76,13 @@ sub handle_login {
|
|||
print "HTTP/1.1 100 Continue\r\n\r\n";
|
||||
}
|
||||
|
||||
#my $content = q{};
|
||||
if ( !$chunked ) {
|
||||
my $buffer;
|
||||
binmode(STDIN);
|
||||
if ( defined $ENV{'MOD_PERL'} ) {
|
||||
while ( read( STDIN, $buffer, $length ) ) {
|
||||
$content .= $buffer;
|
||||
last if ( length($content) >= $length );
|
||||
}
|
||||
} else {
|
||||
while ( sysread( STDIN, $buffer, $length ) ) {
|
||||
$content .= $buffer;
|
||||
last if ( length($content) >= $length );
|
||||
}
|
||||
}
|
||||
## Line added so CGI doesn't try to slurp in the POST content after XMLRPC
|
||||
undef $ENV{CONTENT_LENGTH};
|
||||
}
|
||||
|
@ -147,22 +122,108 @@ sub handle_login {
|
|||
$self->response->content;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 ($name && !$self->response->headers->header($name)) {
|
||||
$self->response->headers->header($name => $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
# This exists to validate input parameters (which XMLRPC::Lite doesn't do)
|
||||
# and also, in some cases, to more-usefully decode them.
|
||||
package Bugzilla::XMLRPC::Deserializer;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
# We can't use "use base" because XMLRPC::Serializer doesn't return
|
||||
use warnings;
|
||||
|
||||
# We can't use "use parent" because XMLRPC::Serializer doesn't return
|
||||
# a true value.
|
||||
use XMLRPC::Lite;
|
||||
our @ISA = qw(XMLRPC::Deserializer);
|
||||
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Constants qw(ERROR_MODE_DIE_SOAP_FAULT);
|
||||
use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST);
|
||||
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;
|
||||
}
|
||||
|
||||
sub deserialize {
|
||||
# SOAP::Lite resets SIG{__DIE__} in the beginning of handle() method, so we set it again here
|
||||
if ($Bugzilla::Error::HAVE_DEVEL_STACKTRACE) {
|
||||
Bugzilla->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
|
||||
$SIG{__DIE__} = \&Bugzilla::_die_error;
|
||||
}
|
||||
|
||||
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 ($xml) = @_;
|
||||
my $som = $self->SUPER::deserialize(@_);
|
||||
if (tainted($xml)) {
|
||||
|
@ -172,7 +233,13 @@ sub deserialize {
|
|||
my $params = $som->paramsin;
|
||||
# This allows positional parameters for Testopia.
|
||||
$params = {} if ref $params ne 'HASH';
|
||||
|
||||
# Update the params to allow for several convenience key/values
|
||||
# use for authentication
|
||||
fix_credentials($params);
|
||||
|
||||
Bugzilla->input_params($params);
|
||||
|
||||
return $som;
|
||||
}
|
||||
|
||||
|
@ -236,7 +303,11 @@ sub _validation_subs {
|
|||
1;
|
||||
|
||||
package Bugzilla::XMLRPC::SOM;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use XMLRPC::Lite;
|
||||
our @ISA = qw(XMLRPC::SOM);
|
||||
use Bugzilla::WebService::Util qw(taint_data);
|
||||
|
@ -259,9 +330,13 @@ sub paramsin {
|
|||
# This package exists to fix a UTF-8 bug in SOAP::Lite.
|
||||
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
|
||||
package Bugzilla::XMLRPC::Serializer;
|
||||
use Scalar::Util qw(blessed);
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
# We can't use "use base" because XMLRPC::Serializer doesn't return
|
||||
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);
|
||||
|
@ -295,7 +370,7 @@ sub envelope {
|
|||
my ($type, $method, $data) = @_;
|
||||
# If the type isn't a successful response we don't want to change the values.
|
||||
if ($type eq 'response') {
|
||||
$data = _strip_undefs($data);
|
||||
_strip_undefs($data);
|
||||
}
|
||||
return $self->SUPER::envelope($type, $method, $data);
|
||||
}
|
||||
|
@ -306,7 +381,9 @@ sub envelope {
|
|||
# so it cannot be recursed like the other hash type objects.
|
||||
sub _strip_undefs {
|
||||
my ($initial) = @_;
|
||||
if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
|
||||
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) )
|
||||
|
@ -315,11 +392,11 @@ sub _strip_undefs {
|
|||
delete $initial->{$key};
|
||||
}
|
||||
else {
|
||||
$initial->{$key} = _strip_undefs($value);
|
||||
_strip_undefs($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
|
||||
elsif ($type eq "ARRAY") {
|
||||
for (my $count = 0; $count < scalar @{$initial}; $count++) {
|
||||
my $value = $initial->[$count];
|
||||
if ( !defined $value
|
||||
|
@ -330,11 +407,10 @@ sub _strip_undefs {
|
|||
$count--;
|
||||
}
|
||||
else {
|
||||
$initial->[$count] = _strip_undefs($value);
|
||||
_strip_undefs($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $initial;
|
||||
}
|
||||
|
||||
sub BEGIN {
|
||||
|
@ -436,3 +512,15 @@ perl-SOAP-Lite package in versions 0.68-1 and above.
|
|||
=head1 SEE ALSO
|
||||
|
||||
L<Bugzilla::WebService>
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item make_response
|
||||
|
||||
=item initialize
|
||||
|
||||
=item handle_login
|
||||
|
||||
=back
|
||||
|
|
|
@ -1,25 +1,14 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# Mads Bondo Dydensborg <mbd@dbc.dk>
|
||||
# Noura Elhawary <nelhawar@redhat.com>
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::User;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use base qw(Bugzilla::WebService);
|
||||
|
||||
use Bugzilla;
|
||||
|
@ -27,8 +16,10 @@ use Bugzilla::Constants;
|
|||
use Bugzilla::Error;
|
||||
use Bugzilla::Group;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Util qw(trim);
|
||||
use Bugzilla::WebService::Util qw(filter validate);
|
||||
use Bugzilla::Util qw(trim detaint_natural);
|
||||
use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects user_to_hash);
|
||||
|
||||
use List::Util qw(first min);
|
||||
|
||||
# Don't need auth to login
|
||||
use constant LOGIN_EXEMPT => {
|
||||
|
@ -40,44 +31,72 @@ use constant READ_ONLY => qw(
|
|||
get
|
||||
);
|
||||
|
||||
use constant PUBLIC_METHODS => qw(
|
||||
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_RETURNS => {
|
||||
login_name => 'email',
|
||||
realname => 'full_name',
|
||||
disabledtext => 'login_denied_text',
|
||||
};
|
||||
|
||||
##############
|
||||
# User Login #
|
||||
##############
|
||||
|
||||
sub login {
|
||||
my ($self, $params) = @_;
|
||||
my $remember = $params->{remember};
|
||||
|
||||
# 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->{$param} || defined $params->{'Bugzilla_' . $param})
|
||||
|| ThrowCodeError('param_required', { param => $param });
|
||||
}
|
||||
|
||||
# Convert $remember from a boolean 0/1 value to a CGI-compatible one.
|
||||
if (defined($remember)) {
|
||||
$remember = $remember? 'on': '';
|
||||
}
|
||||
else {
|
||||
# Use Bugzilla's default if $remember is not supplied.
|
||||
$remember =
|
||||
Bugzilla->params->{'rememberlogin'} eq 'defaulton'? 'on': '';
|
||||
}
|
||||
|
||||
# Make sure the CGI user info class works if necessary.
|
||||
my $remember = $params->{Bugzilla_remember} || $params->{remember};
|
||||
$remember = defined($remember) ? $remember : Bugzilla->params->{rememberlogin} eq 'defaulton';
|
||||
my $input_params = Bugzilla->input_params;
|
||||
$input_params->{'Bugzilla_login'} = $params->{login};
|
||||
$input_params->{'Bugzilla_password'} = $params->{password};
|
||||
$input_params->{'Bugzilla_remember'} = $remember;
|
||||
$input_params->{Bugzilla_login} = $params->{Bugzilla_login} || $params->{login};
|
||||
$input_params->{Bugzilla_password} = $params->{Bugzilla_password} || $params->{password};
|
||||
$input_params->{Bugzilla_remember} = $remember ? 'on' : '';
|
||||
|
||||
Bugzilla->login();
|
||||
return { id => $self->type('int', Bugzilla->user->id) };
|
||||
$user = Bugzilla->login();
|
||||
return $self->_login_to_hash($user);
|
||||
}
|
||||
|
||||
sub logout {
|
||||
my $self = shift;
|
||||
Bugzilla->logout;
|
||||
return undef;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#################
|
||||
|
@ -128,7 +147,9 @@ sub create {
|
|||
# $call = $rpc->call( 'User.get', { match => [ 'testusera', 'testuserb' ],
|
||||
# maxusermatches => 20, excludedisabled => 1 });
|
||||
sub get {
|
||||
my ($self, $params) = validate(@_, 'names', 'ids');
|
||||
my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
|
||||
|
||||
Bugzilla->switch_to_shadow_db();
|
||||
|
||||
# Make them arrays if they aren't
|
||||
if ($params->{names} && !ref $params->{names}) {
|
||||
|
@ -170,8 +191,8 @@ sub get {
|
|||
\@user_objects, $params);
|
||||
@users = map { filter $params, {
|
||||
id => $self->type('int', $_->id),
|
||||
real_name => $self->type('string', $_->name),
|
||||
name => $self->type('string', $_->login),
|
||||
real_name => $self->type('string', $_->realname),
|
||||
name => $self->type('email', $_->login),
|
||||
} } @$in_group;
|
||||
|
||||
return { users => \@users };
|
||||
|
@ -180,7 +201,7 @@ sub get {
|
|||
my $obj_by_ids;
|
||||
$obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
|
||||
|
||||
# obj_by_ids are only visible to the user if he can see
|
||||
# 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)){
|
||||
|
@ -198,10 +219,15 @@ sub get {
|
|||
}
|
||||
|
||||
# User Matching
|
||||
my $limit;
|
||||
if ($params->{'maxusermatches'}) {
|
||||
$limit = $params->{'maxusermatches'} + 1;
|
||||
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};
|
||||
}
|
||||
|
||||
foreach my $match_string (@{ $params->{'match'} || [] }) {
|
||||
my $matched = Bugzilla::User::match_name($match_string, $limit, $params->{excludedisabled} && 1);
|
||||
foreach my $user (@$matched) {
|
||||
|
@ -212,34 +238,115 @@ sub get {
|
|||
}
|
||||
}
|
||||
|
||||
my $in_group = $self->_filter_users_by_group(
|
||||
\@user_objects, $params);
|
||||
my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
|
||||
foreach my $user (@$in_group) {
|
||||
my $user_info = user_to_hash($self, $user);
|
||||
|
||||
if (Bugzilla->user->in_group('editusers')) {
|
||||
@users =
|
||||
map {filter $params, {
|
||||
id => $self->type('int', $_->id),
|
||||
real_name => $self->type('string', $_->realname),
|
||||
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),
|
||||
}} @$in_group;
|
||||
$user_info->{email_enabled} = $self->type('boolean', $user->email_enabled);
|
||||
$user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
|
||||
}
|
||||
|
||||
if (Bugzilla->user->id == $user->id) {
|
||||
if (filter_wants($params, 'saved_searches')) {
|
||||
$user_info->{saved_searches} = [
|
||||
map { $self->_query_to_hash($_) } @{ $user->queries }
|
||||
];
|
||||
}
|
||||
if (filter_wants($params, 'saved_reports')) {
|
||||
$user_info->{saved_reports} = [
|
||||
map { $self->_report_to_hash($_) } @{ $user->reports }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@users =
|
||||
map {filter $params, {
|
||||
id => $self->type('int', $_->id),
|
||||
real_name => $self->type('string', $_->realname),
|
||||
name => $self->type('string', $_->login),
|
||||
email => $self->type('string', $_->email),
|
||||
can_login => $self->type('boolean', $_->is_enabled),
|
||||
}} @$in_group;
|
||||
$user_info->{groups} = $self->_filter_bless_groups($user->groups);
|
||||
}
|
||||
}
|
||||
|
||||
push(@users, $user_info);
|
||||
}
|
||||
|
||||
return { users => \@users };
|
||||
}
|
||||
|
||||
###############
|
||||
# User Update #
|
||||
###############
|
||||
|
||||
sub update {
|
||||
my ($self, $params) = @_;
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
my $user = Bugzilla->login(LOGIN_REQUIRED);
|
||||
|
||||
# Reject access if there is no sense in continuing.
|
||||
$user->in_group('editusers')
|
||||
|| ThrowUserError("auth_failure", {group => "editusers",
|
||||
action => "edit",
|
||||
object => "users"});
|
||||
|
||||
defined($params->{names}) || defined($params->{ids})
|
||||
|| ThrowCodeError('params_required',
|
||||
{ function => 'User.update', params => ['ids', 'names'] });
|
||||
|
||||
my $user_objects = params_to_objects($params, 'Bugzilla::User');
|
||||
|
||||
my $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 $user (@$user_objects){
|
||||
$user->set_all($values);
|
||||
}
|
||||
|
||||
my %changes;
|
||||
foreach my $user (@$user_objects){
|
||||
my $returned_changes = $user->update();
|
||||
$changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
|
||||
}
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
my @result;
|
||||
foreach my $user (@$user_objects) {
|
||||
my %hash = (
|
||||
id => $user->id,
|
||||
changes => {},
|
||||
);
|
||||
|
||||
foreach my $field (keys %{ $changes{$user->id} }) {
|
||||
my $change = $changes{$user->id}->{$field};
|
||||
# We normalize undef to an empty string, so that the API
|
||||
# stays consistent for things that can become empty.
|
||||
$change->[0] = '' if !defined $change->[0];
|
||||
$change->[1] = '' if !defined $change->[1];
|
||||
# We also flatten arrays (used by groups and blessed_groups)
|
||||
$change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
|
||||
$change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
|
||||
|
||||
$hash{changes}{$field} = {
|
||||
removed => $self->type('string', $change->[0]),
|
||||
added => $self->type('string', $change->[1])
|
||||
};
|
||||
}
|
||||
|
||||
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)};
|
||||
|
@ -247,15 +354,23 @@ sub _filter_users_by_group {
|
|||
# If no groups are specified, we return all users.
|
||||
return $users if (!$group_ids and !$group_names);
|
||||
|
||||
my @groups = map { Bugzilla::Group->check({ id => $_ }) }
|
||||
@{ $group_ids || [] };
|
||||
my @name_groups = map { Bugzilla::Group->check($_) }
|
||||
@{ $group_names || [] };
|
||||
push(@groups, @name_groups);
|
||||
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;
|
||||
|
||||
my @in_group = grep { $self->_user_in_any_group($_, \@groups) }
|
||||
@$users;
|
||||
my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
|
||||
return \@in_group;
|
||||
}
|
||||
|
||||
|
@ -271,12 +386,64 @@ sub read_new_functionality {
|
|||
my $time = time;
|
||||
my $cgi = Bugzilla->cgi;
|
||||
my @lu = map { $_ - 0 } Bugzilla->params->{new_functionality_tsp} =~ m/(\d+)/g;
|
||||
my $last_updated = POSIX::mktime(@lu[5], @lu[4], @lu[3], @lu[2], @lu[1] - 1, @lu[0] - 1900);
|
||||
my $last_updated = POSIX::mktime($lu[5], $lu[4], $lu[3], $lu[2], $lu[1] - 1, $lu[0] - 1900);
|
||||
$time = $last_updated + 1 if $time < $last_updated;
|
||||
$cgi->send_cookie('-name', 'read_new_functionality', '-value', $time);
|
||||
return {status => 'ok'};
|
||||
}
|
||||
|
||||
sub _filter_bless_groups {
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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->query),
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
@ -295,13 +462,19 @@ log in/out using an existing account.
|
|||
See L<Bugzilla::WebService> for a description of how parameters are passed,
|
||||
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
|
||||
|
||||
=head2 Logging In and Out
|
||||
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
|
||||
the directions for how to access the data via REST is noted in each method
|
||||
where applicable.
|
||||
|
||||
=over
|
||||
=head1 Logging In and Out
|
||||
|
||||
=item C<login>
|
||||
These method are now deprecated, and will be removed in the release after
|
||||
Bugzilla 5.0. The correct way of use these REST and RPC calls is noted in
|
||||
L<Bugzilla::WebService>
|
||||
|
||||
B<STABLE>
|
||||
=head2 login
|
||||
|
||||
B<DEPRECATED>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -319,22 +492,19 @@ etc. This method logs in an user.
|
|||
|
||||
=item C<password> (string) - The user's password.
|
||||
|
||||
=item C<remember> (bool) B<Optional> - if the cookies returned by the
|
||||
call to login should expire with the session or not. In order for
|
||||
this option to have effect the Bugzilla server must be configured to
|
||||
allow the user to set this option - the Bugzilla parameter
|
||||
I<rememberlogin> must be set to "defaulton" or
|
||||
"defaultoff". Addionally, the client application must implement
|
||||
management of cookies across sessions.
|
||||
=item C<restrict_login> (bool) B<Optional> - If set to a true value,
|
||||
the token returned by this method will only be valid from the IP address
|
||||
which called this method.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
On success, a hash containing one item, C<id>, the numeric id of the
|
||||
user that was logged in. A set of http cookies is also sent with the
|
||||
response. These cookies must be sent along with any future requests
|
||||
to the webservice, for the duration of the session.
|
||||
On success, a hash containing two items, C<id>, the numeric id of the
|
||||
user that was logged in, and a C<token> which can be passed in
|
||||
the parameters as authentication in other calls. The token can be sent
|
||||
along with any future requests to the webservice, for the duration of the
|
||||
session, i.e. till L<User.logout|/logout> is called.
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
|
@ -344,15 +514,15 @@ to the webservice, for the duration of the session.
|
|||
|
||||
The username does not exist, or the password is wrong.
|
||||
|
||||
=item 301 (Account Disabled)
|
||||
=item 301 (Login Disabled)
|
||||
|
||||
The account has been disabled. A reason may be specified with the
|
||||
error.
|
||||
The ability to login with this account has been disabled. A reason may be
|
||||
specified with the error.
|
||||
|
||||
=item 305 (New Password Required)
|
||||
|
||||
The current password is correct, but the user is asked to change
|
||||
his password.
|
||||
their password.
|
||||
|
||||
=item 50 (Param Required)
|
||||
|
||||
|
@ -360,11 +530,26 @@ A login or password parameter was not provided.
|
|||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item C<remember> was removed in Bugzilla B<5.0> as this method no longer
|
||||
creates a login cookie.
|
||||
|
||||
=item C<restrict_login> was added in Bugzilla B<5.0>.
|
||||
|
||||
=item C<token> was added in Bugzilla B<4.4.3>.
|
||||
|
||||
=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
|
||||
|
||||
=back
|
||||
|
||||
=item C<logout>
|
||||
=back
|
||||
|
||||
B<STABLE>
|
||||
=head2 logout
|
||||
|
||||
B<DEPRECATED>
|
||||
|
||||
=over
|
||||
|
||||
|
@ -380,13 +565,55 @@ Log out the user. Does nothing if there is no user logged in.
|
|||
|
||||
=back
|
||||
|
||||
=back
|
||||
=head2 valid_login
|
||||
|
||||
=head2 Account Creation
|
||||
B<DEPRECATED>
|
||||
|
||||
=over
|
||||
|
||||
=item C<offer_account_by_email>
|
||||
=item B<Description>
|
||||
|
||||
This method will verify whether a client's cookies or current login
|
||||
token is still valid or have expired. A valid username must be provided
|
||||
as well that matches.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<login>
|
||||
|
||||
The login name that matches the provided cookies or token.
|
||||
|
||||
=item C<token>
|
||||
|
||||
(string) Persistent login token current being used for authentication (optional).
|
||||
Cookies passed by client will be used before the token if both provided.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
Returns true/false depending on if the current cookies or token are valid
|
||||
for the provided username.
|
||||
|
||||
=item B<Errors> (none)
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item Added in Bugzilla B<5.0>.
|
||||
|
||||
=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 Account Creation and Modification
|
||||
|
||||
=head2 offer_account_by_email
|
||||
|
||||
B<STABLE>
|
||||
|
||||
|
@ -427,7 +654,7 @@ email address you specified. Account creation may be entirely disabled.
|
|||
|
||||
=back
|
||||
|
||||
=item C<create>
|
||||
=head2 create
|
||||
|
||||
B<STABLE>
|
||||
|
||||
|
@ -443,6 +670,13 @@ actually receive an email. This function does not check that.
|
|||
You must be logged in and have the C<editusers> privilege in order to
|
||||
call this function.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
POST /rest/user
|
||||
|
||||
The params to include in the POST body as well as the returned data format,
|
||||
are the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
@ -486,17 +720,160 @@ password is under three characters.)
|
|||
|
||||
=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
|
||||
|
||||
=back
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head2 User Info
|
||||
=head2 update
|
||||
|
||||
B<EXPERIMENTAL>
|
||||
|
||||
=over
|
||||
|
||||
=item C<get>
|
||||
=item B<Description>
|
||||
|
||||
Updates user accounts in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
PUT /rest/user/<user_id_or_name>
|
||||
|
||||
The params to include in the PUT body as well as the returned data format,
|
||||
are the same as below. The C<ids> and C<names> params are overridden as they
|
||||
are pulled from the URL path.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
=over
|
||||
|
||||
=item C<ids>
|
||||
|
||||
C<array> Contains ids of user to update.
|
||||
|
||||
=item C<names>
|
||||
|
||||
C<array> Contains email/login of user to update.
|
||||
|
||||
=item C<full_name>
|
||||
|
||||
C<string> The new name of the user.
|
||||
|
||||
=item C<email>
|
||||
|
||||
C<string> The email of the user. Note that email used to login to bugzilla.
|
||||
Also note that you can only update one user at a time when changing the
|
||||
login name / email. (An error will be thrown if you try to update this field
|
||||
for multiple users at once.)
|
||||
|
||||
=item C<password>
|
||||
|
||||
C<string> The password of the user.
|
||||
|
||||
=item C<email_enabled>
|
||||
|
||||
C<boolean> A boolean value to enable/disable sending bug-related mail to the user.
|
||||
|
||||
=item C<login_denied_text>
|
||||
|
||||
C<string> A text field that holds the reason for disabling a user from logging
|
||||
into bugzilla, if empty then the user account is enabled otherwise it is
|
||||
disabled/closed.
|
||||
|
||||
=item C<groups>
|
||||
|
||||
C<hash> These specify the groups that this user is directly a member of.
|
||||
To set these, you should pass a hash as the value. The hash may contain
|
||||
the following fields:
|
||||
|
||||
=over
|
||||
|
||||
=item C<add> An array of C<int>s or C<string>s. The group ids or group names
|
||||
that the user should be added to.
|
||||
|
||||
=item C<remove> An array of C<int>s or C<string>s. The group ids or group names
|
||||
that the user should be removed from.
|
||||
|
||||
=item C<set> An array of C<int>s or C<string>s. An exact set of group ids
|
||||
and group names that the user should be a member of. NOTE: This does not
|
||||
remove groups from the user where the person making the change does not
|
||||
have the bless privilege for.
|
||||
|
||||
If you specify C<set>, then C<add> and C<remove> will be ignored. A group in
|
||||
both the C<add> and C<remove> list will be added. Specifying a group that the
|
||||
user making the change does not have bless rights will generate an error.
|
||||
|
||||
=back
|
||||
|
||||
=item C<bless_groups>
|
||||
|
||||
C<hash> - This is the same as groups, but affects what groups a user
|
||||
has direct membership to bless that group. It takes the same inputs as
|
||||
groups.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
||||
A C<hash> with a single field "users". This points to an array of hashes
|
||||
with the following fields:
|
||||
|
||||
=over
|
||||
|
||||
=item C<id>
|
||||
|
||||
C<int> The id of the user that was updated.
|
||||
|
||||
=item C<changes>
|
||||
|
||||
C<hash> The changes that were actually done on this user. The keys are
|
||||
the names of the fields that were changed, and the values are a hash
|
||||
with two keys:
|
||||
|
||||
=over
|
||||
|
||||
=item C<added>
|
||||
|
||||
C<string> The values that were added to this field,
|
||||
possibly a comma-and-space-separated list if multiple values were added.
|
||||
|
||||
=item C<removed>
|
||||
|
||||
C<string> The values that were removed from this field, possibly a
|
||||
comma-and-space-separated list if multiple values were removed.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=item B<Errors>
|
||||
|
||||
=over
|
||||
|
||||
=item 51 (Bad Login Name)
|
||||
|
||||
You passed an invalid login name in the "names" array.
|
||||
|
||||
=item 304 (Authorization Required)
|
||||
|
||||
Logged-in users are not authorized to edit other users.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
||||
=over
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
=back
|
||||
|
||||
=head1 User Info
|
||||
|
||||
=head2 get
|
||||
|
||||
B<STABLE>
|
||||
|
||||
|
@ -506,6 +883,18 @@ B<STABLE>
|
|||
|
||||
Gets information about user accounts in Bugzilla.
|
||||
|
||||
=item B<REST>
|
||||
|
||||
To get information about a single user:
|
||||
|
||||
GET /rest/user/<user_id_or_name>
|
||||
|
||||
To search for users by name, group using URL params same as below:
|
||||
|
||||
GET /rest/user
|
||||
|
||||
The returned data format is the same as below.
|
||||
|
||||
=item B<Params>
|
||||
|
||||
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
|
||||
|
@ -538,9 +927,6 @@ Bugzilla itself. Users will be returned whose real name or login name
|
|||
contains any one of the specified strings. Users that you cannot see will
|
||||
not be included in the returned list.
|
||||
|
||||
Some Bugzilla installations have user-matching turned off, in which
|
||||
case you will only be returned exact matches.
|
||||
|
||||
Most installations have a limit on how many matches are returned for
|
||||
each string, which defaults to 1000 but can be changed by the Bugzilla
|
||||
administrator.
|
||||
|
@ -550,6 +936,13 @@ if they try. (This is to make it harder for spammers to harvest email
|
|||
addresses from Bugzilla, and also to enforce the user visibility
|
||||
restrictions that are implemented on some Bugzillas.)
|
||||
|
||||
=item C<limit> (int)
|
||||
|
||||
Limit the number of users matched by the C<match> parameter. If value
|
||||
is greater than the system limit, the system limit will be used. This
|
||||
parameter is only used when user matching using the C<match> parameter
|
||||
is being performed.
|
||||
|
||||
=item C<group_ids> (array)
|
||||
|
||||
=item C<groups> (array)
|
||||
|
@ -559,6 +952,14 @@ C<groups> is an array of names of groups that a user can be in.
|
|||
If these are specified, they limit the return value to users who are
|
||||
in I<any> of the groups specified.
|
||||
|
||||
=item C<include_disabled> (boolean)
|
||||
|
||||
By default, when using the C<match> parameter, disabled users are excluded
|
||||
from the returned results unless their full username is identical to the
|
||||
match string. Setting C<include_disabled> to C<true> will include disabled
|
||||
users in the returned results even if their username doesn't fully match
|
||||
the input string.
|
||||
|
||||
=back
|
||||
|
||||
=item B<Returns>
|
||||
|
@ -601,10 +1002,79 @@ C<string> A text field that holds the reason for disabling a user from logging
|
|||
into bugzilla, if empty then the user account is enabled. Otherwise it is
|
||||
disabled/closed.
|
||||
|
||||
=item groups
|
||||
|
||||
C<array> An array of group hashes the user is a member of. If the currently
|
||||
logged in user is querying their own account or is a member of the 'editusers'
|
||||
group, the array will contain all the groups that the user is a
|
||||
member of. Otherwise, the array will only contain groups that the logged in
|
||||
user can bless. Each hash describes the group and contains the following items:
|
||||
|
||||
=over
|
||||
|
||||
=item id
|
||||
|
||||
C<int> The group id
|
||||
|
||||
=item name
|
||||
|
||||
C<string> The name of the group
|
||||
|
||||
=item description
|
||||
|
||||
C<string> The description for the group
|
||||
|
||||
=back
|
||||
|
||||
=item saved_searches
|
||||
|
||||
C<array> An array of hashes, each of which represents a user's saved search and has
|
||||
the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item id
|
||||
|
||||
C<int> An integer id uniquely identifying the saved search.
|
||||
|
||||
=item name
|
||||
|
||||
C<string> The name of the saved search.
|
||||
|
||||
=item query
|
||||
|
||||
C<string> The CGI parameters for the saved search.
|
||||
|
||||
=back
|
||||
|
||||
=item saved_reports
|
||||
|
||||
C<array> An array of hashes, each of which represents a user's saved report and has
|
||||
the following keys:
|
||||
|
||||
=over
|
||||
|
||||
=item id
|
||||
|
||||
C<int> An integer id uniquely identifying the saved report.
|
||||
|
||||
=item name
|
||||
|
||||
C<string> The name of the saved report.
|
||||
|
||||
=item query
|
||||
|
||||
C<string> The CGI parameters for the saved report.
|
||||
|
||||
=back
|
||||
|
||||
B<Note>: If you are not logged in to Bugzilla when you call this function, you
|
||||
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
|
||||
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
|
||||
C<real_name>, C<email>, and C<can_login> items.
|
||||
C<real_name>, C<email>, C<can_login>, and C<groups> items. The groups returned are
|
||||
filtered based on your permission to bless each group.
|
||||
The C<saved_searches> and C<saved_reports> items are only returned if you are
|
||||
querying your own account, even if you are in the editusers group.
|
||||
|
||||
=back
|
||||
|
||||
|
@ -612,10 +1082,14 @@ C<real_name>, C<email>, and C<can_login> items.
|
|||
|
||||
=over
|
||||
|
||||
=item 51 (Bad Login Name or Group Name)
|
||||
=item 51 (Bad Login Name or Group ID)
|
||||
|
||||
You passed an invalid login name in the "names" array or a bad
|
||||
group name/id in the C<groups>/C<group_ids> arguments.
|
||||
group ID in the C<group_ids> argument.
|
||||
|
||||
=item 52 (Invalid Parameter)
|
||||
|
||||
The value used must be an integer greater than zero.
|
||||
|
||||
=item 304 (Authorization Required)
|
||||
|
||||
|
@ -627,6 +1101,11 @@ wanted to get information about by user id.
|
|||
Logged-out users cannot use the "ids" or "match" arguments to this
|
||||
function.
|
||||
|
||||
=item 804 (Invalid Group Name)
|
||||
|
||||
You passed a group name in the C<groups> argument which either does not
|
||||
exist or you do not belong to it.
|
||||
|
||||
=back
|
||||
|
||||
=item B<History>
|
||||
|
@ -637,7 +1116,16 @@ function.
|
|||
|
||||
=item C<group_ids> and C<groups> were added in Bugzilla B<4.0>.
|
||||
|
||||
=back
|
||||
=item C<include_disabled> was added in Bugzilla B<4.0>. Default
|
||||
behavior for C<match> was changed to only return enabled accounts.
|
||||
|
||||
=item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now
|
||||
illegal to pass a group name you don't belong to.
|
||||
|
||||
=item C<groups>, C<saved_searches>, and C<saved_reports> were added
|
||||
in Bugzilla B<4.4>.
|
||||
|
||||
=item REST API call added in Bugzilla B<5.0>.
|
||||
|
||||
=back
|
||||
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
# -*- Mode: perl; indent-tabs-mode: nil -*-
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
#
|
||||
# The contents of this file are subject to the Mozilla Public
|
||||
# License Version 1.1 (the "License"); you may not use this file
|
||||
# except in compliance with the License. You may obtain a copy of
|
||||
# the License at http://www.mozilla.org/MPL/
|
||||
#
|
||||
# Software distributed under the License is distributed on an "AS
|
||||
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
|
||||
# implied. See the License for the specific language governing
|
||||
# rights and limitations under the License.
|
||||
#
|
||||
# The Original Code is the Bugzilla Bug Tracking System.
|
||||
#
|
||||
# The Initial Developer of the Original Code is Everything Solved, Inc.
|
||||
# Portions created by the Initial Developer are Copyright (C) 2008
|
||||
# the Initial Developer. All Rights Reserved.
|
||||
#
|
||||
# Contributor(s):
|
||||
# Max Kanat-Alexander <mkanat@bugzilla.org>
|
||||
# This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
# defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
package Bugzilla::WebService::Util;
|
||||
|
||||
use 5.10.1;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use Bugzilla::Flag;
|
||||
use Bugzilla::FlagType;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Util qw(list);
|
||||
|
||||
use Storable qw(dclone);
|
||||
|
||||
use base qw(Exporter);
|
||||
|
||||
# We have to "require", not "use" this, because otherwise it tries to
|
||||
|
@ -28,36 +25,166 @@ use base qw(Exporter);
|
|||
require Test::Taint;
|
||||
|
||||
our @EXPORT_OK = qw(
|
||||
extract_flags
|
||||
filter
|
||||
filter_wants
|
||||
taint_data
|
||||
validate
|
||||
translate
|
||||
params_to_objects
|
||||
user_to_hash
|
||||
fix_credentials
|
||||
);
|
||||
|
||||
sub filter ($$) {
|
||||
my ($params, $hash) = @_;
|
||||
sub extract_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;
|
||||
|
||||
# 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};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
foreach my $key (keys %$hash) {
|
||||
delete $newhash{$key} if !filter_wants($params, $key);
|
||||
delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
|
||||
}
|
||||
|
||||
return \%newhash;
|
||||
}
|
||||
|
||||
sub filter_wants ($$) {
|
||||
my ($params, $field) = @_;
|
||||
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
|
||||
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
|
||||
sub filter_wants($$;$$) {
|
||||
my ($params, $field, $types, $prefix) = @_;
|
||||
|
||||
if (defined $params->{include_fields}) {
|
||||
return 0 if !$include{$field};
|
||||
}
|
||||
if (defined $params->{exclude_fields}) {
|
||||
return 0 if $exclude{$field};
|
||||
# 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};
|
||||
}
|
||||
|
||||
return 1;
|
||||
# Mimic old behavior if no types provided
|
||||
my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
|
||||
|
||||
my %include = map { $_ => 1 } list($params->{include_fields});
|
||||
my %exclude = map { $_ => 1 } list($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 {
|
||||
|
@ -77,8 +204,9 @@ sub _delete_bad_keys {
|
|||
# Making something a hash key always untaints it, in Perl.
|
||||
# However, we need to validate our argument names in some way.
|
||||
# We know that all hash keys passed in to the WebService will
|
||||
# match \w+, so we delete any key that doesn't match that.
|
||||
if ($key !~ /^\w+$/) {
|
||||
# match \w+, contain '.' or '-', so we delete any key that
|
||||
# doesn't match that.
|
||||
if ($key !~ /^[\w\.\-]+$/) {
|
||||
delete $item->{$key};
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +236,67 @@ sub validate {
|
|||
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;
|
||||
}
|
||||
|
||||
sub params_to_objects {
|
||||
my ($params, $class) = @_;
|
||||
my (@objects, @objects_by_ids);
|
||||
|
||||
@objects = map { $class->check($_) }
|
||||
@{ $params->{names} } if $params->{names};
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
sub user_to_hash {
|
||||
my ($ws, $user, $params) = @_;
|
||||
return filter $params, {
|
||||
id => $ws->type('int', $user->id),
|
||||
real_name => $ws->type('string', $user->realname),
|
||||
name => $ws->type('email', $user->login),
|
||||
email => $ws->type('email', $user->email),
|
||||
can_login => $ws->type('boolean', $user->is_enabled ? 1 : 0),
|
||||
};
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
@ -129,25 +318,64 @@ internally in the WebService code.
|
|||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item C<filter>
|
||||
=head2 filter
|
||||
|
||||
This helps implement the C<include_fields> and C<exclude_fields> arguments
|
||||
of WebService methods. Given a hash (the second argument to this subroutine),
|
||||
this will remove any keys that are I<not> in C<include_fields> and then remove
|
||||
any keys that I<are> in C<exclude_fields>.
|
||||
|
||||
=item C<filter_wants>
|
||||
An optional third option can be passed that prefixes the field name to allow
|
||||
filtering of data two or more levels deep.
|
||||
|
||||
For example, if you want to filter out the C<id> key/value in components returned
|
||||
by Product.get, you would use the value C<component.id> in your C<exclude_fields>
|
||||
list.
|
||||
|
||||
=head2 filter_wants
|
||||
|
||||
Returns C<1> if a filter would preserve the specified field when passing
|
||||
a hash to L</filter>, C<0> otherwise.
|
||||
|
||||
=item C<validate>
|
||||
=head2 validate
|
||||
|
||||
This helps in the validation of parameters passed into the WebSerice
|
||||
This helps in the validation of parameters passed into the WebService
|
||||
methods. Currently it converts listed parameters into an array reference
|
||||
if the client only passed a single scalar value. It modifies the parameters
|
||||
hash in place so other parameters should be unaltered.
|
||||
|
||||
=head2 translate
|
||||
|
||||
WebService methods frequently take parameters with different names than
|
||||
the ones that we use internally in Bugzilla. This function takes a hashref
|
||||
that has field names for keys and returns a hashref with those keys renamed
|
||||
according to the mapping passed in with the second parameter (which is also
|
||||
a hashref).
|
||||
|
||||
=head2 params_to_objects
|
||||
|
||||
Creates objects of the type passed in as the second parameter, using the
|
||||
parameters passed to a WebService method (the first parameter to this function).
|
||||
Helps make life simpler for WebService methods that internally create objects
|
||||
via both "ids" and "names" fields. Also de-duplicates objects that were loaded
|
||||
by both "ids" and "names". Returns an arrayref of objects.
|
||||
|
||||
=head2 fix_credentials
|
||||
|
||||
Allows for certain parameters related to authentication such as Bugzilla_login,
|
||||
Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
|
||||
This function converts the shorter versions to their respective internal names.
|
||||
|
||||
=head2 extract_flags
|
||||
|
||||
Subroutine that takes a list of hashes that are potential flag changes for
|
||||
both bugs and attachments. Then breaks the list down into two separate lists
|
||||
based on if the change is to add a new flag or to update an existing flag.
|
||||
|
||||
=head1 B<Methods in need of POD>
|
||||
|
||||
=over
|
||||
|
||||
=item taint_data
|
||||
|
||||
=back
|
||||
|
|
|
@ -40,6 +40,7 @@ use constant DB_COLUMNS => qw(
|
|||
sortkey
|
||||
onemailperbug
|
||||
title
|
||||
isreport
|
||||
);
|
||||
|
||||
use constant NAME_FIELD => 'id';
|
||||
|
@ -53,7 +54,7 @@ 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 isreport { return $_[0]->{'isreport'}; }
|
||||
|
||||
1;
|
||||
|
||||
|
|
33
CHANGELOG
33
CHANGELOG
|
@ -1,10 +1,41 @@
|
|||
== UNRELEASED: beta ==
|
||||
== branch: i18n ==
|
||||
|
||||
UI improvements:
|
||||
|
||||
* Separate localisation layer - now it's possible to translate Bugzilla UI
|
||||
without copying templates!
|
||||
|
||||
== UNRELEASED: beta ==
|
||||
|
||||
== 2016.09: 2016-09-01, commit 55bee6cdccae3a83200c799a23a4fda4d135717f ==
|
||||
|
||||
Backports from original Bugzilla:
|
||||
|
||||
* WebServices from Bugzilla 5.0.1 (everything except tags).
|
||||
|
||||
Other new features:
|
||||
|
||||
* Separate "createproducts" role (group) that only allows to create new products,
|
||||
but not to edit the existing ones.
|
||||
* Support custom saved search columns in whining
|
||||
* Support saved reports in whining
|
||||
* Support multi-select columns of bugs referenced by bug type custom fields in search
|
||||
* Check saved search runner permissions correctly in "matched by saved search" operator when query is shared
|
||||
* Report all XML-RPC exceptions
|
||||
* Add Group.{add,remove}_{members,managers} web services
|
||||
* Add "bypass group" parameters to Checkers
|
||||
* Support multiple "look for bug in..." URLs
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Whining now works again.
|
||||
* Fix&rework whining
|
||||
* Silence CGI.pm warnings
|
||||
* Fix `deadline` field history logging
|
||||
* Fix "totals" in table reports
|
||||
* Do not crash in Search when multiple values are passed to a single-value operator
|
||||
* Several other fixes
|
||||
|
||||
== 2015.09: 2015-09-18, commit dc07c69094f616d1eb3e523181283dcdbca0499e ==
|
||||
|
||||
UI improvements:
|
||||
|
|
|
@ -12,8 +12,6 @@ BEGIN
|
|||
($dir) = $dir =~ /^(.*)$/s;
|
||||
chdir($dir);
|
||||
$Bugzilla::HTTPServerSimple::DOCROOT = $dir;
|
||||
# Force everyone to use buffered input!
|
||||
*CORE::GLOBAL::sysread = sub(*$$;$) { read $_[0], $_[1], $_[2], $_[3]; };
|
||||
}
|
||||
|
||||
use lib qw(.);
|
||||
|
@ -120,6 +118,7 @@ sub new
|
|||
{
|
||||
for (glob $script)
|
||||
{
|
||||
($_) = /^(.*)$/so;
|
||||
eval
|
||||
{
|
||||
$self->load_script($_);
|
||||
|
@ -252,6 +251,7 @@ sub run_script
|
|||
my ($script) = @_;
|
||||
$self->load_script($script);
|
||||
my $start = [gettimeofday];
|
||||
$Bugzilla::Error::IN_EVAL++;
|
||||
$in_eval = 1;
|
||||
eval { &{$subs{$script}}(); };
|
||||
$self->check_errors($script);
|
||||
|
@ -289,6 +289,7 @@ sub check_errors
|
|||
print STDERR "Error in _cleanup():\n$@";
|
||||
}
|
||||
$in_eval = 0;
|
||||
$Bugzilla::Error::IN_EVAL--;
|
||||
if ($err)
|
||||
{
|
||||
$self->internal_error($err);
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
package Text::TabularDisplay::Utf8;
|
||||
|
||||
use utf8;
|
||||
use strict;
|
||||
use base 'Text::TabularDisplay';
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# render([$start, $end])
|
||||
#
|
||||
# Returns the data formatted as a table. By default, all rows are
|
||||
# returned; if $start or $end are specified, then only those indexes
|
||||
# are returned. Those are the start and end indexes!
|
||||
# -------------------------------------------------------------------
|
||||
sub render {
|
||||
my $self = shift;
|
||||
my $start = shift || 0;
|
||||
my $end = shift || $#{ $self->{ _DATA } };
|
||||
my $size = $self->{ _SIZE };
|
||||
my (@columns, $datum, @text);
|
||||
|
||||
push @text, '┌' . join("┬", map( { "─" x ($_ + 2) } @{ $self->{ _LENGTHS } })) . '┐';
|
||||
|
||||
if (@columns = $self->columns) {
|
||||
push @text, _format_line(\@columns, $self->{ _LENGTHS });
|
||||
push @text, '├' . join("┼", map( { "─" x ($_ + 2) } @{ $self->{ _LENGTHS } })) . '┤';
|
||||
}
|
||||
|
||||
for (my $i = $start; $i <= $end; $i++) {
|
||||
$datum = $self->{ _DATA }->[$i];
|
||||
last unless defined $datum;
|
||||
|
||||
# Pad the array if there are more elements in @columns
|
||||
push @$datum, ""
|
||||
until (@$datum == $size);
|
||||
push @text, _format_line($datum, $self->{ _LENGTHS });
|
||||
}
|
||||
|
||||
push @text, '└' . join("┴", map( { "─" x ($_ + 2) } @{ $self->{ _LENGTHS } })) . '┘';
|
||||
return join "\n", @text;
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# _column_length($str)
|
||||
# -------------------------------------------------------------------
|
||||
sub _column_length
|
||||
{
|
||||
my ($str) = @_;
|
||||
|
||||
my $len = 0;
|
||||
for (split "\n", $str) {
|
||||
$len = length
|
||||
if $len < length;
|
||||
}
|
||||
# why the /hell/ this length is tainted?..
|
||||
if (${^TAINT})
|
||||
{
|
||||
($len) = $len =~ /(\d+)/so;
|
||||
}
|
||||
|
||||
return $len;
|
||||
}
|
||||
|
||||
undef &Text::TabularDisplay::_column_length;
|
||||
*Text::TabularDisplay::_column_length = \&_column_length;
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# _format_line(\@columns, \@lengths)
|
||||
#
|
||||
# Returns a formatted line out of @columns; the size of $column[$i]
|
||||
# is determined by $length[$i].
|
||||
# -------------------------------------------------------------------
|
||||
sub _format_line {
|
||||
my ($columns, $lengths) = @_;
|
||||
|
||||
my $height = 0;
|
||||
my @col_lines;
|
||||
for (@$columns) {
|
||||
my @lines = split "\n";
|
||||
$height = scalar @lines
|
||||
if $height < @lines;
|
||||
push @col_lines, \@lines;
|
||||
}
|
||||
|
||||
my @lines;
|
||||
for my $h (0 .. $height - 1 ) {
|
||||
my @line;
|
||||
for (my $i = 0; $i <= $#$columns; $i++) {
|
||||
my $val = defined($col_lines[$i][$h]) ? $col_lines[$i][$h] : '';
|
||||
push @line, sprintf " %-" . $lengths->[$i] . "s ", $val;
|
||||
}
|
||||
push @lines, join '│', "", @line, "";
|
||||
}
|
||||
|
||||
return join "\n", @lines;
|
||||
}
|
||||
|
||||
1;
|
||||
__END__
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Text::TabularDisplay::Utf8 - Display text in formatted table output using UTF-8 pseudographics
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
use Text::TabularDisplay::Utf8;
|
||||
|
||||
my $table = Text::TabularDisplay::Utf8->new(@columns);
|
||||
$table->add(@row)
|
||||
while (@row = $sth->fetchrow);
|
||||
print $table->render;
|
||||
|
||||
┌────┬──────────────┐
|
||||
│ id │ name │
|
||||
├────┼──────────────┤
|
||||
│ 1 │ Tom │
|
||||
│ 2 │ Dick │
|
||||
│ 3 │ Barry │
|
||||
│ │ (aka Bazza) │
|
||||
│ 4 │ Harry │
|
||||
└────┴──────────────┘
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The program interface is fully compatible with C<Text::TabularDisplay> -
|
||||
see its perldoc for more information.
|
|
@ -515,6 +515,7 @@ sub enter
|
|||
my $comment = $ARGS->{comment};
|
||||
$comment = '' unless defined $comment;
|
||||
$vars->{commenttext} = $comment;
|
||||
$vars->{work_time} = $ARGS->{work_time};
|
||||
|
||||
# Generate and return the UI (HTML page) from the appropriate template.
|
||||
Bugzilla->template->process("attachment/create.html.tmpl", $vars)
|
||||
|
|
320
buglist.cgi
320
buglist.cgi
|
@ -183,28 +183,16 @@ my $format = $superworktime ? "worktime/supertime" : "list/list";
|
|||
$format = $template->get_format($format, $ARGS->{format}, $ARGS->{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
|
||||
# the bug list as HTML and they have not disabled it by adding &serverpush=0
|
||||
# to the URL.
|
||||
#
|
||||
# Server push is a Netscape 3+ hack incompatible with MSIE, Lynx, and others.
|
||||
# Even Communicator 4.51 has bugs with it, especially during page reload.
|
||||
# http://www.browsercaps.org used as source of compatible browsers.
|
||||
# Safari (WebKit) does not support it, despite a UA that says otherwise (bug 188712)
|
||||
# MSIE 5+ supports it on Mac (but not on Windows) (bug 190370)
|
||||
#
|
||||
my $serverpush =
|
||||
$format->{extension} eq "html"
|
||||
# executing their query, but only in Firefox, as only Firefox supports is reliably.
|
||||
# IE11 on Win8.1 mimics itself as Mozilla, Edge mimics itself as all browsers at once.
|
||||
# Happily both have 'like Gecko' in their UA string.
|
||||
my $serverpush = $format->{extension} eq "html"
|
||||
&& exists $ENV{HTTP_USER_AGENT}
|
||||
&& $ENV{HTTP_USER_AGENT} =~ /Mozilla.[3-9]/
|
||||
&& (($ENV{HTTP_USER_AGENT} !~ /[Cc]ompatible/) || ($ENV{HTTP_USER_AGENT} =~ /MSIE 5.*Mac_PowerPC/))
|
||||
&& $ENV{HTTP_USER_AGENT} !~ /WebKit/
|
||||
&& !$agent
|
||||
&& !defined($ARGS->{serverpush})
|
||||
&& $ENV{HTTP_USER_AGENT} !~ /compatible|msie|webkit|like\s*gecko/i
|
||||
&& !$agent && !defined($ARGS->{serverpush})
|
||||
|| $ARGS->{serverpush};
|
||||
|
||||
my $order = $ARGS->{order} || "";
|
||||
|
||||
# The params object to use for the actual query itself
|
||||
my $params;
|
||||
|
||||
|
@ -213,36 +201,17 @@ my $params;
|
|||
if (defined $ARGS->{regetlastlist})
|
||||
{
|
||||
my $bug_id = Bugzilla->cookies->{BUGLIST} || ThrowUserError('missing_cookie');
|
||||
$order = 'reuse last sort' unless $order;
|
||||
$ARGS->{order} ||= 'reuse last sort';
|
||||
$bug_id =~ s/:/,/g;
|
||||
# set up the params for this new query
|
||||
$params = {
|
||||
bug_id => $bug_id,
|
||||
bug_id_type => 'anyexact',
|
||||
order => $order,
|
||||
order => $ARGS->{order},
|
||||
columnlist => $ARGS->{columnlist},
|
||||
};
|
||||
}
|
||||
|
||||
# Figure out whether or not the user is doing a fulltext search. If not,
|
||||
# we'll remove the relevance column from the lists of columns to display
|
||||
# and order by, since relevance only exists when doing a fulltext search.
|
||||
my $fulltext = 0;
|
||||
if ($ARGS->{content})
|
||||
{
|
||||
$fulltext = 1;
|
||||
}
|
||||
|
||||
my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), keys %$ARGS);
|
||||
foreach my $chart (@charts)
|
||||
{
|
||||
if ($ARGS->{"field$chart"} eq 'content' && $ARGS->{"value$chart"})
|
||||
{
|
||||
$fulltext = 1;
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Utilities
|
||||
################################################################################
|
||||
|
@ -458,14 +427,12 @@ if ($cmdtype eq "dorem")
|
|||
$vars->{search_id} = $query->id;
|
||||
}
|
||||
$params = http_decode_query($query->query.'&sharer_id='.$query->userid);
|
||||
$order = $params->{order} || $order;
|
||||
}
|
||||
elsif ($remaction eq "runseries")
|
||||
{
|
||||
$vars->{searchname} = $ARGS->{namedcmd};
|
||||
$vars->{searchtype} = "series";
|
||||
$params = http_decode_query(LookupSeries($ARGS->{"series_id"}));
|
||||
$order = $params->{order} || $order;
|
||||
}
|
||||
elsif ($remaction eq 'forget')
|
||||
{
|
||||
|
@ -524,117 +491,38 @@ elsif (($cmdtype eq 'doit') && defined $ARGS->{remtype})
|
|||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Column Definition
|
||||
################################################################################
|
||||
|
||||
my $columns = Bugzilla::Search::COLUMNS;
|
||||
|
||||
################################################################################
|
||||
# Display Column Determination
|
||||
################################################################################
|
||||
|
||||
# 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->{columnlist} && $params->{columnlist} ne 'all')
|
||||
{
|
||||
@displaycolumns = split(/[ ,]+/, $params->{columnlist});
|
||||
}
|
||||
elsif (defined Bugzilla->cookies->{COLUMNLIST})
|
||||
{
|
||||
@displaycolumns = split(/ /, Bugzilla->cookies->{COLUMNLIST});
|
||||
}
|
||||
else
|
||||
{
|
||||
# Use the default list of columns.
|
||||
@displaycolumns = DEFAULT_COLUMN_LIST;
|
||||
}
|
||||
|
||||
$_ = Bugzilla::Search->COLUMN_ALIASES->{$_} || $_ for @displaycolumns;
|
||||
|
||||
# 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);
|
||||
my ($displaycolumns, $selectcolumns) = Bugzilla::Search->get_columns($params, Bugzilla->user);
|
||||
|
||||
# Add the votes column to the list of columns to be displayed
|
||||
# in the bug list if the user is searching for bugs with a certain
|
||||
# number of votes and the votes column is not already on the list.
|
||||
|
||||
# Some versions of perl will taint 'votes' if this is done as a single
|
||||
# statement, because the votes param is tainted at this point
|
||||
my $votes = $params->{votes};
|
||||
$votes ||= "";
|
||||
if (trim($votes) && !grep($_ eq 'votes', @displaycolumns))
|
||||
if (trim($params->{votes} || '') && !grep($_ eq 'votes', @$selectcolumns))
|
||||
{
|
||||
push(@displaycolumns, 'votes');
|
||||
push @$displaycolumns, 'votes';
|
||||
push @$selectcolumns, 'votes';
|
||||
}
|
||||
|
||||
# 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 (!Bugzilla->user->is_timetracker)
|
||||
if ($superworktime && !grep($_ eq 'interval_time', @$selectcolumns))
|
||||
{
|
||||
@displaycolumns = grep { !TIMETRACKING_FIELDS->{$_} } @displaycolumns;
|
||||
}
|
||||
|
||||
# Remove the relevance column if the user is not doing a fulltext search.
|
||||
if (grep('relevance', @displaycolumns) && !$fulltext)
|
||||
{
|
||||
@displaycolumns = grep($_ ne 'relevance', @displaycolumns);
|
||||
}
|
||||
|
||||
# Remove the "ID" column from the list because bug IDs are always displayed
|
||||
# and are hard-coded into the display templates.
|
||||
@displaycolumns = grep($_ ne 'bug_id', @displaycolumns);
|
||||
|
||||
if ($superworktime && !grep($_ eq 'interval_time', @displaycolumns))
|
||||
{
|
||||
push @displaycolumns, 'interval_time';
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Select Column Determination
|
||||
################################################################################
|
||||
|
||||
# Generate the list of columns that will be selected in the SQL query.
|
||||
|
||||
# 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");
|
||||
|
||||
# Make sure that the login_name version of a field is always also
|
||||
# requested if the realname version is requested, so that we can
|
||||
# display the login name when the realname is empty.
|
||||
my @realname_fields = grep(/_realname$|_short$/, @displaycolumns);
|
||||
foreach my $item (@realname_fields)
|
||||
{
|
||||
my $login_field = $item;
|
||||
$login_field =~ s/_realname$|_short$//;
|
||||
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 @$displaycolumns, 'interval_time';
|
||||
push @$selectcolumns, 'interval_time';
|
||||
}
|
||||
|
||||
# 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)
|
||||
if ($dotweak && !grep($_ eq 'bug_status', @$selectcolumns))
|
||||
{
|
||||
push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
|
||||
push @$selectcolumns, "bug_status";
|
||||
}
|
||||
|
||||
if ($format->{extension} eq 'ics')
|
||||
if ($format->{extension} eq 'ics' && !grep($_ eq 'creation_ts', @$selectcolumns))
|
||||
{
|
||||
push(@selectcolumns, "creation_ts") if !grep($_ eq 'creation_ts', @selectcolumns);
|
||||
push @$selectcolumns, "creation_ts";
|
||||
}
|
||||
|
||||
if ($format->{extension} eq 'atom')
|
||||
|
@ -662,102 +550,26 @@ if ($format->{extension} eq 'atom')
|
|||
|
||||
foreach my $required (@required_atom_columns)
|
||||
{
|
||||
push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns);
|
||||
push(@$selectcolumns, $required) if !grep($_ eq $required, @$selectcolumns);
|
||||
}
|
||||
}
|
||||
|
||||
if ($superworktime && !grep($_ eq 'product_notimetracking', @displaycolumns))
|
||||
if ($superworktime && !grep($_ eq 'product_notimetracking', @$displaycolumns))
|
||||
{
|
||||
push @selectcolumns, 'product_notimetracking';
|
||||
push @$selectcolumns, 'product_notimetracking';
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Sort Order Determination
|
||||
################################################################################
|
||||
|
||||
# Add to the query some instructions for sorting the bug list.
|
||||
|
||||
# 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 ($order = Bugzilla->cookies->{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
|
||||
}
|
||||
}
|
||||
|
||||
# FIXME Move sort orders to Bugzilla::Search
|
||||
my $old_orders = {
|
||||
'' => 'bug_status,priority,assigned_to,bug_id', # Default
|
||||
'bug number' => 'bug_id',
|
||||
'importance' => 'priority,bug_severity,bug_id',
|
||||
'assignee' => 'assigned_to,bug_status,priority,bug_id',
|
||||
'last changed' => 'delta_ts,bug_status,priority,assigned_to,bug_id',
|
||||
};
|
||||
if ($order)
|
||||
{
|
||||
# Convert the value of the "order" form field into a list of columns
|
||||
# by which to sort the results.
|
||||
if ($old_orders->{lc $order})
|
||||
{
|
||||
$order = $old_orders->{lc $order};
|
||||
}
|
||||
else
|
||||
{
|
||||
my (@order, @invalid_fragments);
|
||||
|
||||
# A custom list of columns. Make sure each column is valid.
|
||||
foreach my $fragment (split(/,/, $order))
|
||||
{
|
||||
$fragment = trim($fragment);
|
||||
next unless $fragment;
|
||||
my ($column_name, $direction) = Bugzilla::Search::split_order_term($fragment);
|
||||
$column_name = Bugzilla::Search::translate_old_column($column_name);
|
||||
|
||||
# Special handlings for certain columns
|
||||
next if $column_name eq 'relevance' && !$fulltext;
|
||||
|
||||
# If we are sorting by votes, sort in descending order if
|
||||
# no explicit sort order was given.
|
||||
if ($column_name eq 'votes' && !$direction)
|
||||
{
|
||||
$direction = "DESC";
|
||||
}
|
||||
|
||||
if (exists $columns->{$column_name})
|
||||
{
|
||||
$direction = " $direction" if $direction;
|
||||
push @order, "$column_name$direction";
|
||||
}
|
||||
else
|
||||
{
|
||||
push @invalid_fragments, $fragment;
|
||||
}
|
||||
}
|
||||
if (scalar @invalid_fragments)
|
||||
my ($orderstrings, $invalid_fragments) = Bugzilla::Search->get_order($params, Bugzilla->user);
|
||||
if (scalar @$invalid_fragments)
|
||||
{
|
||||
$vars->{message} = 'invalid_column_name';
|
||||
$vars->{invalid_fragments} = \@invalid_fragments;
|
||||
$vars->{invalid_fragments} = $invalid_fragments;
|
||||
}
|
||||
|
||||
$order = join(",", @order);
|
||||
# Now that we have checked that all columns in the order are valid,
|
||||
# detaint the order string.
|
||||
trick_taint($order) if $order;
|
||||
}
|
||||
}
|
||||
|
||||
$order = $old_orders->{''} if !$order;
|
||||
|
||||
my @orderstrings = split(/,\s*/, $order);
|
||||
|
||||
# The bug status defined by a specific search is of type __foo__, but
|
||||
# Search.pm converts it into a list of real bug statuses, which cannot
|
||||
# be used when editing the specific search again. So we restore this
|
||||
|
@ -770,9 +582,9 @@ if ($query_format eq 'specific')
|
|||
|
||||
# Generate the basic SQL query that will be used to generate the bug list.
|
||||
my $search = new Bugzilla::Search(
|
||||
'fields' => \@selectcolumns,
|
||||
'fields' => $selectcolumns,
|
||||
'params' => $params,
|
||||
'order' => \@orderstrings
|
||||
'order' => $orderstrings,
|
||||
);
|
||||
my $query = $search->getSQL();
|
||||
$vars->{search_description} = $search->search_description_html;
|
||||
|
@ -798,13 +610,10 @@ if (defined $ARGS->{limit})
|
|||
$query .= " " . $dbh->sql_limit($limit);
|
||||
}
|
||||
}
|
||||
elsif ($fulltext)
|
||||
{
|
||||
if ($ARGS->{order} && $ARGS->{order} =~ /^relevance/)
|
||||
elsif ($ARGS->{order} && $ARGS->{order} =~ /^relevance/)
|
||||
{
|
||||
$vars->{message} = 'buglist_sorted_by_relevance';
|
||||
}
|
||||
}
|
||||
|
||||
if ($superworktime)
|
||||
{
|
||||
|
@ -898,22 +707,16 @@ $buglist_sth->execute();
|
|||
|
||||
# Retrieve the query results one row at a time and write the data into a list of Perl records.
|
||||
|
||||
# If we're doing time tracking, then keep totals for all bugs.
|
||||
my $percentage_complete = 1 && grep { $_ eq 'percentage_complete' } @displaycolumns;
|
||||
my $estimated_time = 1 && grep { $_ eq 'estimated_time' } @displaycolumns;
|
||||
my $remaining_time = $percentage_complete || grep { $_ eq 'remaining_time' } @displaycolumns;
|
||||
my $work_time = $percentage_complete || grep { $_ eq 'work_time' } @displaycolumns;
|
||||
my $interval_time = $percentage_complete || grep { $_ eq 'interval_time' } @displaycolumns;
|
||||
|
||||
my $time_info = {
|
||||
estimated_time => 0,
|
||||
remaining_time => 0,
|
||||
work_time => 0,
|
||||
percentage_complete => 0,
|
||||
interval_time => 0, # CustIS Bug 68921
|
||||
time_present => ($estimated_time || $remaining_time ||
|
||||
$work_time || $percentage_complete || $interval_time),
|
||||
};
|
||||
# Calculate totals
|
||||
my $total_info;
|
||||
for my $column (@$displaycolumns)
|
||||
{
|
||||
if (Bugzilla::Search->COLUMNS->{$column}->{numeric})
|
||||
{
|
||||
$total_info ||= {};
|
||||
$total_info->{$column} = 0;
|
||||
}
|
||||
}
|
||||
|
||||
my $bugowners = {};
|
||||
my $bugproducts = {};
|
||||
|
@ -929,7 +732,7 @@ while (my @row = $buglist_sth->fetchrow_array())
|
|||
# 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)
|
||||
foreach my $column (@$selectcolumns)
|
||||
{
|
||||
$bug->{$column} = shift @row;
|
||||
}
|
||||
|
@ -938,12 +741,6 @@ while (my @row = $buglist_sth->fetchrow_array())
|
|||
if ($bug->{delta_ts})
|
||||
{
|
||||
$bug->{delta_ts} =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
|
||||
$bug->{delta_ts_diff} = DiffDate($bug->{delta_ts});
|
||||
}
|
||||
|
||||
if ($bug->{creation_ts})
|
||||
{
|
||||
$bug->{creation_ts_diff} = DiffDate($bug->{creation_ts});
|
||||
}
|
||||
|
||||
# Record the assignee, product, and status in the big hashes of those things.
|
||||
|
@ -960,10 +757,10 @@ while (my @row = $buglist_sth->fetchrow_array())
|
|||
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->{work_time} += $bug->{work_time} if $work_time;
|
||||
$time_info->{interval_time} += $bug->{interval_time} if $interval_time;
|
||||
for my $column (keys %$total_info)
|
||||
{
|
||||
$total_info->{$column} += $bug->{$column} if $column ne 'percentage_complete';
|
||||
}
|
||||
}
|
||||
|
||||
my $query_template_time = gettimeofday();
|
||||
|
@ -1003,15 +800,18 @@ if (@bugidlist)
|
|||
}
|
||||
|
||||
# Compute percentage complete without rounding.
|
||||
my $sum = $time_info->{work_time} + $time_info->{remaining_time};
|
||||
if (exists $total_info->{percentage_complete})
|
||||
{
|
||||
my $sum = $total_info->{work_time} + $total_info->{remaining_time};
|
||||
if ($sum > 0)
|
||||
{
|
||||
$time_info->{percentage_complete} = 100*$time_info->{work_time}/$sum;
|
||||
$total_info->{percentage_complete} = 100*$total_info->{work_time}/$sum;
|
||||
}
|
||||
else
|
||||
{
|
||||
# remaining_time <= 0
|
||||
$time_info->{percentage_complete} = 0
|
||||
$total_info->{percentage_complete} = 0
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
@ -1023,8 +823,7 @@ else
|
|||
$vars->{bugs} = \@bugs;
|
||||
$vars->{buglist} = \@bugidlist;
|
||||
$vars->{buglist_joined} = join(',', @bugidlist);
|
||||
$vars->{columns} = $columns;
|
||||
$vars->{displaycolumns} = \@displaycolumns;
|
||||
$vars->{displaycolumns} = $displaycolumns;
|
||||
|
||||
$vars->{openstates} = [ map { $_->name } grep { $_->is_open } Bugzilla::Status->get_all ];
|
||||
# used by list.ics.tmpl
|
||||
|
@ -1070,12 +869,12 @@ if ($vars->{urlquerypart}->{sharer_id})
|
|||
}
|
||||
$vars->{urlquerypart} = http_build_query($vars->{urlquerypart});
|
||||
$vars->{rssquerypart} = $vars->{urlquerypart};
|
||||
$vars->{order} = $order;
|
||||
$vars->{order_columns} = [ @orderstrings ];
|
||||
$vars->{order} = $params->{order};
|
||||
$vars->{order_columns} = $orderstrings;
|
||||
$vars->{order_dir} = [ map { s/ DESC$// ? 1 : 0 } @{$vars->{order_columns}} ];
|
||||
|
||||
$vars->{caneditbugs} = 1;
|
||||
$vars->{time_info} = $time_info;
|
||||
$vars->{total_info} = $total_info;
|
||||
|
||||
$vars->{query_params} = { %$params }; # now used only in superworktime
|
||||
$vars->{query_params}->{chfieldfrom} = $search->{interval_from};
|
||||
|
@ -1179,6 +978,7 @@ if ($dotweak && scalar @bugs)
|
|||
my $visible = {};
|
||||
for my $field (Bugzilla->active_custom_fields)
|
||||
{
|
||||
next if $field->type == FIELD_TYPE_BUG_ID_REV;
|
||||
my $vis_field = $field->visibility_field;
|
||||
my $vis = 1;
|
||||
if ($vis_field)
|
||||
|
@ -1231,8 +1031,7 @@ sub get_bug_vals
|
|||
my $field_name = $field->name;
|
||||
my $type = $field->value_type;
|
||||
my $id_field = $type->ID_FIELD;
|
||||
my $m = $field_name;
|
||||
$m .= '_obj' if $field_name eq 'product' || $field_name eq 'component'; # FIXME remove when product_obj replaces product
|
||||
my $m = $field_name.'_obj';
|
||||
my $ids = {};
|
||||
my $v;
|
||||
for my $bug (@$bugs)
|
||||
|
@ -1257,6 +1056,7 @@ $vars->{query_sql_time} = sprintf("%.2f", $query_sql_time);
|
|||
|
||||
Bugzilla::Hook::process('after-buglist', { vars => $vars });
|
||||
|
||||
# FIXME: Remove this hardcode
|
||||
$vars->{abbrev} = {
|
||||
bug_severity => { maxlength => 3, title => "Sev" },
|
||||
priority => { maxlength => 3, title => "Pri" },
|
||||
|
@ -1271,8 +1071,8 @@ $vars->{abbrev} = {
|
|||
resolution => { maxlength => 4 },
|
||||
short_short_desc => { maxlength => 60, ellipsis => "..." },
|
||||
status_whiteboard => { title => "Whiteboard" },
|
||||
component => { maxlength => 8, title => "Comp" },
|
||||
product => { maxlength => 8 },
|
||||
component => { maxlength => 20, ellipsis => "...", title => "Comp" },
|
||||
product => { maxlength => 20, ellipsis => "..." },
|
||||
op_sys => { maxlength => 4 },
|
||||
target_milestone => { title => "Milestone" },
|
||||
percentage_complete => { format_value => "%d %%" },
|
||||
|
@ -1291,11 +1091,11 @@ my $disposition = "inline";
|
|||
|
||||
if ($format->{extension} eq "html" && !$agent)
|
||||
{
|
||||
if ($order && !$ARGS->{sharer_id} && $query_format ne 'specific')
|
||||
if ($ARGS->{order} && !$ARGS->{sharer_id} && $query_format ne 'specific')
|
||||
{
|
||||
$cgi->send_cookie(
|
||||
-name => 'LASTORDER',
|
||||
-value => $order,
|
||||
-value => $ARGS->{order},
|
||||
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ use Bugzilla::Keyword;
|
|||
use Bugzilla::Product;
|
||||
use Bugzilla::Status;
|
||||
use Bugzilla::Field;
|
||||
use Bugzilla::Util qw(list);
|
||||
|
||||
use Digest::MD5 qw(md5_base64);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Add this to your sphinx.conf to use Sphinx search
|
||||
# and then set $sphinx_index, $sphinx_host, $sphinx_port, $sphinx_sock, and $sphinxse_port in ../localconfig
|
||||
|
||||
index bugs
|
||||
{
|
||||
|
@ -7,9 +8,8 @@ index bugs
|
|||
rt_field = short_desc
|
||||
rt_field = comments
|
||||
rt_field = comments_private
|
||||
docinfo = extern
|
||||
enable_star = 1
|
||||
charset_type = utf-8
|
||||
rt_attr_uint = x
|
||||
ondisk_attrs = 1
|
||||
charset_table = 0..9, A..Z->a..z, a..z, U+410..U+42F->U+430..U+44F, U+430..U+44F
|
||||
blend_chars = _, -, &, +, @, $
|
||||
morphology = stem_enru
|
|
@ -1,7 +0,0 @@
|
|||
# Nothing in this directory is retrievable unless overridden by an .htaccess
|
||||
# in a subdirectory; the only exception is duplicates.rdf, which is used by
|
||||
# duplicates.xul and must be accessible from the web server
|
||||
deny from all
|
||||
<Files duplicates.rdf>
|
||||
allow from all
|
||||
</Files>
|
|
@ -1,5 +0,0 @@
|
|||
1
|
||||
include .
|
||||
exclude data/params.bugs3
|
||||
exclude data/params.bugs3-sm
|
||||
exclude data/hgsvn-filemap
|
|
@ -1,171 +0,0 @@
|
|||
%param = (
|
||||
'LDAPBaseDN' => '',
|
||||
'LDAPbinddn' => '',
|
||||
'LDAPfilter' => '',
|
||||
'LDAPmailattribute' => 'mail',
|
||||
'LDAPserver' => '',
|
||||
'LDAPstarttls' => 0,
|
||||
'LDAPuidattribute' => 'uid',
|
||||
'RADIUS_NAS_IP' => '',
|
||||
'RADIUS_email_suffix' => '',
|
||||
'RADIUS_secret' => '',
|
||||
'RADIUS_server' => '',
|
||||
'allow-test-deletion' => '1',
|
||||
'allow_attach_url' => 0,
|
||||
'allow_attachment_deletion' => 0,
|
||||
'allow_attachment_display' => '1',
|
||||
'allowbugdeletion' => 0,
|
||||
'allowemailchange' => 0,
|
||||
'allowuserdeletion' => 0,
|
||||
'announcehtml' => '',
|
||||
'attachment_base' => '',
|
||||
'auth_env_email' => 'REMOTE_USER',
|
||||
'auth_env_id' => '',
|
||||
'auth_env_realname' => '',
|
||||
'auto_add_flag_requestees_to_cc' => '1',
|
||||
'bonsai_url' => '',
|
||||
'bug-to-test-case-action' => 'Verify that bug %id% is fixed: %description%',
|
||||
'bug-to-test-case-results' => '',
|
||||
'bug-to-test-case-summary' => 'Test for bug %id% - %summary%',
|
||||
'chartgroup' => 'editbugs',
|
||||
'clear_requests_on_close' => '1',
|
||||
'commentonchange_resolution' => 0,
|
||||
'commentonduplicate' => 0,
|
||||
'confirmuniqueusermatch' => 1,
|
||||
'cookiedomain' => 'bugs.office.custis.ru',
|
||||
'cookiepath' => '/bugs',
|
||||
'createemailregexp' => '.*',
|
||||
'cvsroot' => '',
|
||||
'cvsroot_get' => '',
|
||||
'default-test-case-status' => 'CONFIRMED',
|
||||
'defaultopsys' => 'All',
|
||||
'defaultplatform' => 'All',
|
||||
'defaultpriority' => 'P3',
|
||||
'defaultquery' => 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&order=Importance&long_desc_type=substring',
|
||||
'defaultseverity' => 'normal',
|
||||
'docs_urlbase' => 'docs/%lang%/html/',
|
||||
'duplicate_or_move_bug_status' => 'RESOLVED',
|
||||
'emailin_autoregister' => 1,
|
||||
'emailregexp' => '^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$',
|
||||
'emailregexpdesc' => 'A legal address must contain exactly one \'@\', and at least one \'.\' after the @.',
|
||||
'emailsuffix' => '',
|
||||
'error_log' => 'errorlog',
|
||||
'ext_disable_refresh_views' => 0,
|
||||
'fof_sudo_mynetworks' => '127.0.0.1/32,172.29.0.6/32',
|
||||
'fof_sudo_server' => 'http://feeds.office.custis.ru/fof-sudo.php',
|
||||
'force_attach_bigfile' => 1,
|
||||
'globalwatchers' => '',
|
||||
'graph_font' => '/usr/share/fonts/truetype/windows/seguibk.ttf',
|
||||
'graph_font_size' => '9',
|
||||
'graph_rankdir' => 'LR',
|
||||
'inbound_proxies' => '',
|
||||
'inline_attachment_mime' => '^text/|^image/',
|
||||
'insidergroup' => 'InsiderGroup',
|
||||
'letsubmitterchoosemilestone' => 1,
|
||||
'letsubmitterchoosepriority' => 1,
|
||||
'levenshteinusermatch' => '0.4',
|
||||
'localdottimeout' => '10',
|
||||
'login_lockout_interval' => '5',
|
||||
'login_urlbase_redirects' => '[^\\@]+\\@custis\\.ru$ http://bugs.office.custis.ru/bugs/
|
||||
[^\\@]+\\@(sportmaster\\.ru|ilion\\.ru|sportmaster\\.com\\.ua|scn\\.ru|mbr\\.ru|ilion\\.ru|vek\\.ru|bis\\.overta\\.ru) http://penguin.office.custis.ru/bugzilla/',
|
||||
'lxr_root' => '',
|
||||
'lxr_url' => '',
|
||||
'mail_delivery_method' => 'Sendmail',
|
||||
'mailfrom' => 'bugzilla-daemon@custis.ru',
|
||||
'maintainer' => 'vfilippov@custis.ru',
|
||||
'makeproductgroups' => '0',
|
||||
'max_login_attempts' => '50',
|
||||
'maxattachmentsize' => '5000',
|
||||
'maxlocalattachment' => '1000',
|
||||
'maxusermatches' => '1000',
|
||||
'mediawiki_urls' => 'wiki http://wiki.office.custis.ru/wiki/index.php/
|
||||
smwiki http://penguin.office.custis.ru/smwiki/index.php/
|
||||
sbwiki http://sobin.office.custis.ru/sbwiki/index.php/
|
||||
rdwiki http://radey.office.custis.ru/rdwiki/index.php/
|
||||
gzwiki http://gazprom.office.custis.ru/gzwiki/index.php/
|
||||
gzstable http://gazprom.office.custis.ru/gzstable/index.php/
|
||||
dpwiki http://depobraz.office.custis.ru/dpwiki/index.php/
|
||||
hrwiki http://hrwiki.office.custis.ru/hrwiki/index.php/
|
||||
cbwiki http://cbr.office.custis.ru/cbwiki/index.php/
|
||||
orwiki http://lodge.office.custis.ru/orwiki/index.php/
|
||||
rawiki http://rawiki.office.custis.ru/rawiki/index.php/
|
||||
crmwiki http://crm.office.custis.ru/
|
||||
itwiki http://itwiki.office.custis.ru/itwiki/index.php/',
|
||||
'mime_types_file' => '/etc/mime.types',
|
||||
'mostfreqthreshold' => '2',
|
||||
'move-button-text' => 'Move To Bugscape',
|
||||
'move-enabled' => '1',
|
||||
'move-to-address' => 'bugzilla-import',
|
||||
'move-to-url' => '',
|
||||
'moved-default-component' => 'dev',
|
||||
'moved-default-product' => 'Bugzilla',
|
||||
'moved-from-address' => 'bugzilla-admin',
|
||||
'movers' => '',
|
||||
'musthavemilestoneonaccept' => 0,
|
||||
'mybugstemplate' => 'buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&email1=%userid%&emailtype1=exact&emailassigned_to1=1&emailreporter1=1',
|
||||
'new-case-action-template' => '',
|
||||
'new-case-results-template' => '',
|
||||
'noresolveonopenblockers' => '0',
|
||||
'proxy_url' => 'http://proxy.custis.ru:3128/',
|
||||
'querysharegroup' => 'editbugs',
|
||||
'quip_list_entry_control' => 'open',
|
||||
'rememberlogin' => 'on',
|
||||
'report_code_errors_to_maintainer' => '1',
|
||||
'report_user_errors_to_maintainer' => '0',
|
||||
'requirelogin' => '1',
|
||||
'sendmailnow' => 1,
|
||||
'shadowdb' => '',
|
||||
'shadowdbhost' => '',
|
||||
'shadowdbport' => '3306',
|
||||
'shadowdbsock' => '',
|
||||
'shutdownhtml' => '',
|
||||
'sm_dotproject_login' => 'ws-zis',
|
||||
'sm_dotproject_password' => '',
|
||||
'sm_dotproject_ws_user' => 'ws-sur@sportmaster.ru',
|
||||
'sm_dotproject_wsdl_url' => 'http://sur.moscow.sportmaster.ru/dotproject/services/index.php?wsdl',
|
||||
'smtp_debug' => 0,
|
||||
'smtp_password' => '',
|
||||
'smtp_username' => '',
|
||||
'smtpserver' => 'localhost',
|
||||
'specific_search_allow_empty_words' => '1',
|
||||
'ssl_redirect' => 0,
|
||||
'sslbase' => '',
|
||||
'stem_language' => 'ru',
|
||||
'strict_isolation' => 0,
|
||||
'supa_jar_url' => '',
|
||||
'test_case_wiki_action_iframe' => '<iframe src="$URL?useskin=ichick" width="100%" height="100%"></iframe>',
|
||||
'testopia-allow-group-member-deletes' => 0,
|
||||
'testopia-debug' => 'ON',
|
||||
'testopia-default-plan-testers-regexp' => undef,
|
||||
'testopia-hide-htmleditor' => '1',
|
||||
'testopia-max-allowed-plan-testers' => '500',
|
||||
'testopia-show-setup-breakdown' => 0,
|
||||
'testopia_sync_password' => 'tester',
|
||||
'testopia_sync_user' => 'TestBot',
|
||||
'timetrackinggroup' => "\x{432}\x{435}\x{441}\x{44c} \x{417}\x{418}\x{421}",
|
||||
'unauth_bug_details' => '1',
|
||||
'upgrade_notification' => 'latest_stable_release',
|
||||
'urlbase' => 'http://bugs.office.custis.ru/bugs/',
|
||||
'use_mailer_queue' => 0,
|
||||
'use_see_also' => 0,
|
||||
'use_supa_applet' => '1',
|
||||
'usebugaliases' => 1,
|
||||
'useclassification' => 1,
|
||||
'usemenuforusers' => '0',
|
||||
'useopsys' => 0,
|
||||
'useplatform' => 0,
|
||||
'useqacontact' => 1,
|
||||
'user_info_class' => 'FOF_Sudo,Env,CGI',
|
||||
'user_mailto' => 'http://plantime.office.custis.ru/CurrentPresence.aspx?search=',
|
||||
'user_verify_class' => 'DB',
|
||||
'usestatuswhiteboard' => 1,
|
||||
'usetargetmilestone' => 1,
|
||||
'usevisibilitygroups' => 0,
|
||||
'usevotes' => 0,
|
||||
'utf8' => 1,
|
||||
'viewvc_url' => 'http://viewvc.office.custis.ru/viewvc.py/',
|
||||
'webdotbase' => '/usr/bin/dot',
|
||||
'webtwopibase' => '/usr/bin/twopi',
|
||||
'whinedays' => '30',
|
||||
'wiki_url' => 'http://wiki.office.custis.ru/wiki/index.php/'
|
||||
);
|
|
@ -1,171 +0,0 @@
|
|||
%param = (
|
||||
'LDAPBaseDN' => '',
|
||||
'LDAPbinddn' => '',
|
||||
'LDAPfilter' => '',
|
||||
'LDAPmailattribute' => 'mail',
|
||||
'LDAPserver' => '',
|
||||
'LDAPstarttls' => 0,
|
||||
'LDAPuidattribute' => 'uid',
|
||||
'RADIUS_NAS_IP' => '',
|
||||
'RADIUS_email_suffix' => '',
|
||||
'RADIUS_secret' => '',
|
||||
'RADIUS_server' => '',
|
||||
'allow-test-deletion' => '1',
|
||||
'allow_attach_url' => 0,
|
||||
'allow_attachment_deletion' => 0,
|
||||
'allow_attachment_display' => '1',
|
||||
'allowbugdeletion' => 0,
|
||||
'allowemailchange' => 0,
|
||||
'allowuserdeletion' => 0,
|
||||
'announcehtml' => '',
|
||||
'attachment_base' => '',
|
||||
'auth_env_email' => 'REMOTE_USER',
|
||||
'auth_env_id' => '',
|
||||
'auth_env_realname' => '',
|
||||
'auto_add_flag_requestees_to_cc' => '1',
|
||||
'bonsai_url' => '',
|
||||
'bug-to-test-case-action' => 'Verify that bug %id% is fixed: %description%',
|
||||
'bug-to-test-case-results' => '',
|
||||
'bug-to-test-case-summary' => 'Test for bug %id% - %summary%',
|
||||
'chartgroup' => 'editbugs',
|
||||
'clear_requests_on_close' => '1',
|
||||
'commentonchange_resolution' => 0,
|
||||
'commentonduplicate' => 0,
|
||||
'confirmuniqueusermatch' => 1,
|
||||
'cookiedomain' => '',
|
||||
'cookiepath' => '/bugzilla',
|
||||
'createemailregexp' => '.*',
|
||||
'cvsroot' => '',
|
||||
'cvsroot_get' => '',
|
||||
'default-test-case-status' => 'CONFIRMED',
|
||||
'defaultopsys' => 'All',
|
||||
'defaultplatform' => 'All',
|
||||
'defaultpriority' => 'P3',
|
||||
'defaultquery' => 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&order=Importance&long_desc_type=substring',
|
||||
'defaultseverity' => 'normal',
|
||||
'docs_urlbase' => 'docs/%lang%/html/',
|
||||
'duplicate_or_move_bug_status' => 'RESOLVED',
|
||||
'emailin_autoregister' => 1,
|
||||
'emailregexp' => '^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$',
|
||||
'emailregexpdesc' => 'A legal address must contain exactly one \'@\', and at least one \'.\' after the @.',
|
||||
'emailsuffix' => '',
|
||||
'error_log' => 'errorlog',
|
||||
'ext_disable_refresh_views' => 1,
|
||||
'fof_sudo_mynetworks' => '127.0.0.1/32,172.29.0.6/32',
|
||||
'fof_sudo_server' => 'http://feeds.office.custis.ru/fof-sudo.php',
|
||||
'force_attach_bigfile' => 1,
|
||||
'globalwatchers' => '',
|
||||
'graph_font' => '/usr/share/fonts/truetype/windows/seguibk.ttf',
|
||||
'graph_font_size' => '9',
|
||||
'graph_rankdir' => 'LR',
|
||||
'inbound_proxies' => '',
|
||||
'inline_attachment_mime' => '^text/|^image/',
|
||||
'insidergroup' => 'admin',
|
||||
'letsubmitterchoosemilestone' => 1,
|
||||
'letsubmitterchoosepriority' => 1,
|
||||
'levenshteinusermatch' => '0.4',
|
||||
'localdottimeout' => 5,
|
||||
'login_lockout_interval' => '5',
|
||||
'login_urlbase_redirects' => '[^\\@]+\\@custis\\.ru$ http://bugs.office.custis.ru/bugs/
|
||||
[^\\@]+\\@(sportmaster\\.ru|ilion\\.ru|sportmaster\\.com\\.ua|scn\\.ru|mbr\\.ru|ilion\\.ru|vek\\.ru|bis\\.overta\\.ru) http://penguin.office.custis.ru/bugzilla/',
|
||||
'lxr_root' => '',
|
||||
'lxr_url' => '',
|
||||
'mail_delivery_method' => 'Sendmail',
|
||||
'mailfrom' => 'bugzilla-daemon@custis.ru',
|
||||
'maintainer' => 'vfilippov@custis.ru',
|
||||
'makeproductgroups' => '1',
|
||||
'max_login_attempts' => '50',
|
||||
'maxattachmentsize' => '5000',
|
||||
'maxlocalattachment' => '1000',
|
||||
'maxusermatches' => '1000',
|
||||
'mediawiki_urls' => 'wiki http://wiki.office.custis.ru/wiki/index.php/
|
||||
smwiki http://penguin.office.custis.ru/smwiki/index.php/
|
||||
sbwiki http://sobin.office.custis.ru/sbwiki/index.php/
|
||||
rdwiki http://radey.office.custis.ru/rdwiki/index.php/
|
||||
gzwiki http://gazprom.office.custis.ru/gzwiki/index.php/
|
||||
gzstable http://gazprom.office.custis.ru/gzstable/index.php/
|
||||
dpwiki http://depobraz.office.custis.ru/dpwiki/index.php/
|
||||
hrwiki http://hrwiki.office.custis.ru/hrwiki/index.php/
|
||||
cbwiki http://cbr.office.custis.ru/cbwiki/index.php/
|
||||
orwiki http://lodge.office.custis.ru/orwiki/index.php/
|
||||
rawiki http://rawiki.office.custis.ru/rawiki/index.php/
|
||||
crmwiki http://crm.office.custis.ru/
|
||||
itwiki http://itwiki.office.custis.ru/itwiki/index.php/',
|
||||
'mime_types_file' => '/etc/mime.types',
|
||||
'mostfreqthreshold' => '2',
|
||||
'move-button-text' => 'Move To Bugscape',
|
||||
'move-enabled' => '1',
|
||||
'move-to-address' => 'bugzilla-import',
|
||||
'move-to-url' => '',
|
||||
'moved-default-component' => 'dev',
|
||||
'moved-default-product' => 'Bugzilla',
|
||||
'moved-from-address' => 'bugzilla-admin',
|
||||
'movers' => '',
|
||||
'musthavemilestoneonaccept' => 0,
|
||||
'mybugstemplate' => 'buglist.cgi?bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&email1=%userid%&emailtype1=exact&emailassigned_to1=1&emailreporter1=1',
|
||||
'new-case-action-template' => '',
|
||||
'new-case-results-template' => '',
|
||||
'noresolveonopenblockers' => '0',
|
||||
'proxy_url' => 'http://proxy.custis.ru:3128/',
|
||||
'querysharegroup' => 'editbugs',
|
||||
'quip_list_entry_control' => 'open',
|
||||
'rememberlogin' => 'on',
|
||||
'report_code_errors_to_maintainer' => '1',
|
||||
'report_user_errors_to_maintainer' => '0',
|
||||
'requirelogin' => '1',
|
||||
'sendmailnow' => 1,
|
||||
'shadowdb' => '',
|
||||
'shadowdbhost' => '',
|
||||
'shadowdbport' => '3306',
|
||||
'shadowdbsock' => '',
|
||||
'shutdownhtml' => '',
|
||||
'sm_dotproject_login' => 'ws-zis',
|
||||
'sm_dotproject_password' => '',
|
||||
'sm_dotproject_ws_user' => 'ws-sur@sportmaster.ru',
|
||||
'sm_dotproject_wsdl_url' => 'http://sur.moscow.sportmaster.ru/dotproject/services/index.php?wsdl',
|
||||
'smtp_debug' => 0,
|
||||
'smtp_password' => '',
|
||||
'smtp_username' => '',
|
||||
'smtpserver' => 'localhost',
|
||||
'specific_search_allow_empty_words' => '1',
|
||||
'ssl_redirect' => 0,
|
||||
'sslbase' => '',
|
||||
'stem_language' => 'ru',
|
||||
'strict_isolation' => 0,
|
||||
'supa_jar_url' => '',
|
||||
'test_case_wiki_action_iframe' => '<iframe src="$URL?useskin=ichick" width="100%" height="100%"></iframe>',
|
||||
'testopia-allow-group-member-deletes' => 0,
|
||||
'testopia-debug' => 'OFF',
|
||||
'testopia-default-plan-testers-regexp' => undef,
|
||||
'testopia-hide-htmleditor' => '1',
|
||||
'testopia-max-allowed-plan-testers' => '500',
|
||||
'testopia-show-setup-breakdown' => '0',
|
||||
'testopia_sync_password' => 'tester',
|
||||
'testopia_sync_user' => 'TestBot',
|
||||
'timetrackinggroup' => "\x{432}\x{435}\x{441}\x{44c} \x{417}\x{418}\x{421}",
|
||||
'unauth_bug_details' => 0,
|
||||
'upgrade_notification' => 'latest_stable_release',
|
||||
'urlbase' => 'http://penguin.office.custis.ru/bugzilla/',
|
||||
'use_mailer_queue' => 0,
|
||||
'use_see_also' => '0',
|
||||
'use_supa_applet' => '1',
|
||||
'usebugaliases' => '1',
|
||||
'useclassification' => 0,
|
||||
'usemenuforusers' => '0',
|
||||
'useopsys' => '0',
|
||||
'useplatform' => '0',
|
||||
'useqacontact' => '1',
|
||||
'user_info_class' => 'Env,CGI',
|
||||
'user_mailto' => 'http://plantime.office.custis.ru/CurrentPresence.aspx?search=',
|
||||
'user_verify_class' => 'DB',
|
||||
'usestatuswhiteboard' => '1',
|
||||
'usetargetmilestone' => '1',
|
||||
'usevisibilitygroups' => 0,
|
||||
'usevotes' => 0,
|
||||
'utf8' => 1,
|
||||
'viewvc_url' => 'http://viewvc.office.custis.ru/viewvc.py/',
|
||||
'webdotbase' => '/usr/bin/dot',
|
||||
'webtwopibase' => '/usr/bin/twopi',
|
||||
'whinedays' => 7,
|
||||
'wiki_url' => 'http://wiki.office.custis.ru/wiki/index.php/'
|
||||
);
|
|
@ -37,8 +37,20 @@ if ($params->{save})
|
|||
{
|
||||
if (/^except_field_(\d+)$/so && $params->{$_})
|
||||
{
|
||||
$except->{$params->{$_}} =
|
||||
$params->{"except_field_$1_value"} || undef;
|
||||
my ($f, $v) = ($params->{$_}, $params->{"except_field_$1_value"});
|
||||
if (!$v)
|
||||
{
|
||||
$except->{$f} = undef;
|
||||
}
|
||||
elsif (!exists $except->{$f})
|
||||
{
|
||||
$except->{$f} = $v;
|
||||
}
|
||||
elsif (defined $except->{$f})
|
||||
{
|
||||
$except->{$f} = [ $except->{$f} ] if !ref $except->{$f};
|
||||
push @{$except->{$f}}, $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
$except = undef if !%$except;
|
||||
|
@ -72,12 +84,14 @@ if ($params->{save})
|
|||
flags => $flags,
|
||||
except_fields => $except,
|
||||
triggers => $triggers,
|
||||
bypass_group_id => $params->{bypass_group_id},
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
$ch = Bugzilla::Checker->check({ id => $id });
|
||||
$ch->set_query_id($params->{query_id});
|
||||
$ch->set('query_id', $params->{query_id});
|
||||
$ch->set('bypass_group_id', $params->{bypass_group_id});
|
||||
$ch->set_message($params->{message});
|
||||
$ch->set_flags($flags);
|
||||
$ch->set_except_fields($except);
|
||||
|
@ -106,6 +120,7 @@ else
|
|||
{
|
||||
$vars->{token} = issue_session_token('editcheckers');
|
||||
$vars->{create} = $params->{create} ? 1 : 0;
|
||||
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
|
||||
# Есть специальное поле "longdesc", означающее добавление комментариев
|
||||
my $f = [ Bugzilla->get_fields ];
|
||||
@$f = sort { lc $a->description cmp lc $b->description } grep { $_->name !~ /
|
||||
|
|
|
@ -82,7 +82,7 @@ LoadTemplate('select') unless $action;
|
|||
if ($action eq 'add')
|
||||
{
|
||||
$vars->{token} = issue_session_token('add_classification');
|
||||
LoadTemplate($action);
|
||||
LoadTemplate('edit');
|
||||
}
|
||||
|
||||
#
|
||||
|
@ -127,7 +127,7 @@ if ($action eq 'del')
|
|||
$vars->{classification} = $classification;
|
||||
$vars->{token} = issue_session_token('delete_classification');
|
||||
|
||||
LoadTemplate($action);
|
||||
LoadTemplate('del');
|
||||
}
|
||||
|
||||
#
|
||||
|
@ -158,7 +158,7 @@ if ($action eq 'edit')
|
|||
$vars->{classification} = $classification;
|
||||
$vars->{token} = issue_session_token('edit_classification');
|
||||
|
||||
LoadTemplate($action);
|
||||
LoadTemplate('edit');
|
||||
}
|
||||
|
||||
#
|
||||
|
@ -224,7 +224,7 @@ if ($action eq 'reclassify')
|
|||
$vars->{classification} = $classification;
|
||||
$vars->{token} = issue_session_token('reclassify_classifications');
|
||||
|
||||
LoadTemplate($action);
|
||||
LoadTemplate('reclassify');
|
||||
}
|
||||
|
||||
#
|
||||
|
|
|
@ -148,7 +148,7 @@ elsif ($action eq 'update')
|
|||
}
|
||||
else
|
||||
{
|
||||
$field->${\$_->[2]}([ list $ARGS->{$_->[3]} ]);
|
||||
$field->${\$_->[2]}([ list $ARGS->{$_->[3]} ], 'SKIP_INVISIBLE');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -106,22 +106,22 @@ sub get_current_and_available
|
|||
push @bless_to_available, $group_option if !$bless_to_current->{$group_option->id};
|
||||
}
|
||||
|
||||
$vars->{members_current} = [ values %$members_current ];
|
||||
$vars->{members_available} = \@members_available;
|
||||
$vars->{member_of_current} = [ values %$member_of_current ];
|
||||
$vars->{member_of_available} = \@member_of_available;
|
||||
$vars->{members_current} = [ sort { $a->name cmp $b->name } values %$members_current ];
|
||||
$vars->{members_available} = [ sort { $a->name cmp $b->name } @members_available ];
|
||||
$vars->{member_of_current} = [ sort { $a->name cmp $b->name } values %$member_of_current ];
|
||||
$vars->{member_of_available} = [ sort { $a->name cmp $b->name } @member_of_available ];
|
||||
|
||||
$vars->{bless_from_current} = [ values %$bless_from_current ];
|
||||
$vars->{bless_from_available} = \@bless_from_available;
|
||||
$vars->{bless_to_current} = [ values %$bless_to_current ];
|
||||
$vars->{bless_to_available} = \@bless_to_available;
|
||||
$vars->{bless_from_current} = [ sort { $a->name cmp $b->name } values %$bless_from_current ];
|
||||
$vars->{bless_from_available} = [ sort { $a->name cmp $b->name } @bless_from_available ];
|
||||
$vars->{bless_to_current} = [ sort { $a->name cmp $b->name } values %$bless_to_current ];
|
||||
$vars->{bless_to_available} = [ sort { $a->name cmp $b->name } @bless_to_available ];
|
||||
|
||||
if (Bugzilla->params->{usevisibilitygroups})
|
||||
{
|
||||
$vars->{visible_from_current} = [ values %$visible_from_current ];
|
||||
$vars->{visible_from_available} = \@visible_from_available;
|
||||
$vars->{visible_to_me_current} = [ values %$visible_to_me_current ];
|
||||
$vars->{visible_to_me_available} = \@visible_to_me_available;
|
||||
$vars->{visible_from_current} = [ sort { $a->name cmp $b->name } values %$visible_from_current ];
|
||||
$vars->{visible_from_available} = [ sort { $a->name cmp $b->name } @visible_from_available ];
|
||||
$vars->{visible_to_me_current} = [ sort { $a->name cmp $b->name } values %$visible_to_me_current ];
|
||||
$vars->{visible_to_me_available} = [ sort { $a->name cmp $b->name } @visible_to_me_available ];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,6 +257,8 @@ if ($action eq 'delete')
|
|||
|
||||
Bugzilla::Hook::process('editgroups-post_delete', { group => $group });
|
||||
Bugzilla::Views::refresh_some_views();
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
|
||||
$vars->{message} = 'group_deleted';
|
||||
ListGroups($vars);
|
||||
|
@ -273,6 +275,8 @@ if ($action eq 'postchanges')
|
|||
|
||||
Bugzilla::Hook::process('editgroups-post_edit', {});
|
||||
Bugzilla::Views::refresh_some_views();
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
|
||||
delete_token($token);
|
||||
|
||||
|
@ -302,6 +306,7 @@ if ($action eq 'confirm_remove')
|
|||
if ($action eq 'remove_regexp')
|
||||
{
|
||||
check_token_data($token, 'remove_group_members');
|
||||
|
||||
# remove all explicit users from the group with
|
||||
# gid = $ARGS->{group} that match the regular expression
|
||||
# stored in the DB for that group or all of them period
|
||||
|
@ -310,26 +315,17 @@ if ($action eq 'remove_regexp')
|
|||
my $regexp = CheckGroupRegexp($ARGS->{regexp});
|
||||
|
||||
$dbh->bz_start_transaction();
|
||||
|
||||
my $users = $group->members_direct();
|
||||
my $sth_delete = $dbh->prepare("DELETE FROM user_group_map WHERE user_id = ? AND isbless = 0 AND group_id = ?");
|
||||
|
||||
my @deleted;
|
||||
foreach my $member (@$users)
|
||||
{
|
||||
if ($regexp eq '' || $member->login =~ m/$regexp/i)
|
||||
{
|
||||
$sth_delete->execute($member->id, $group->id);
|
||||
push @deleted, $member;
|
||||
}
|
||||
}
|
||||
my $del = [ grep { $_->login =~ m/$regexp/is } @{ $group->members_direct } ];
|
||||
$group->remove_users($del, 0);
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
$vars->{users} = \@deleted;
|
||||
$vars->{users} = $del;
|
||||
$vars->{regexp} = $regexp;
|
||||
|
||||
Bugzilla::Hook::process('editgroups-post_remove_regexp', { deleted => \@deleted });
|
||||
Bugzilla::Hook::process('editgroups-post_remove_regexp', { deleted => $del });
|
||||
Bugzilla::Views::refresh_some_views();
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
|
||||
delete_token($token);
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ my $vars = {};
|
|||
$vars->{doc_section} = 'products.html';
|
||||
|
||||
$user->in_group('editcomponents') ||
|
||||
$user->in_group('createproducts') ||
|
||||
scalar(@{$user->get_editable_products}) ||
|
||||
ThrowUserError('auth_failure', {
|
||||
group => 'editcomponents',
|
||||
|
@ -71,7 +72,7 @@ my $useclassification = Bugzilla->get_field('classification')->enabled;
|
|||
# classifications enabled)
|
||||
#
|
||||
|
||||
if ($useclassification && !$classification_name && !$product_name)
|
||||
if (!$action && $useclassification && !$classification_name && !$product_name)
|
||||
{
|
||||
my $class;
|
||||
if ($user->in_group('editcomponents'))
|
||||
|
@ -130,22 +131,21 @@ if (!$action && !$product_name)
|
|||
|
||||
if ($action eq 'add')
|
||||
{
|
||||
# The user must have the global editcomponents privs to add
|
||||
# new products.
|
||||
$user->in_group('editcomponents') || ThrowUserError('auth_failure', {
|
||||
# The user must have the createproducts or global editcomponents privs to add new products.
|
||||
$user->in_group('editcomponents') ||
|
||||
$user->in_group('createproducts') ||
|
||||
ThrowUserError('auth_failure', {
|
||||
group => 'editcomponents',
|
||||
action => 'add',
|
||||
object => 'products',
|
||||
});
|
||||
|
||||
if ($useclassification)
|
||||
{
|
||||
my $classification = Bugzilla::Classification->check($classification_name);
|
||||
$vars->{classification} = $classification;
|
||||
$vars->{classification} = $classification_name ? Bugzilla::Classification->new({ name => $classification_name }) : undef;
|
||||
$vars->{classifications} = [ Bugzilla::Classification->get_all ];
|
||||
}
|
||||
$vars->{token} = issue_session_token('add_product');
|
||||
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
|
||||
$vars->{all_groups} = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
|
||||
|
||||
$template->process('admin/products/create.html.tmpl', $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
@ -159,9 +159,10 @@ if ($action eq 'add')
|
|||
|
||||
if ($action eq 'new')
|
||||
{
|
||||
# The user must have the global editcomponents privs to add
|
||||
# new products.
|
||||
$user->in_group('editcomponents') || ThrowUserError('auth_failure', {
|
||||
# The user must have the createproducts or global editcomponents privs to add new products.
|
||||
$user->in_group('editcomponents') ||
|
||||
$user->in_group('createproducts') ||
|
||||
ThrowUserError('auth_failure', {
|
||||
group => 'editcomponents',
|
||||
action => 'add',
|
||||
object => 'products',
|
||||
|
@ -188,19 +189,31 @@ if ($action eq 'new')
|
|||
$create_params{maxvotesperbug} = $ARGS->{maxvotesperbug};
|
||||
$create_params{votestoconfirm} = $ARGS->{votestoconfirm};
|
||||
}
|
||||
|
||||
Bugzilla->dbh->bz_start_transaction();
|
||||
|
||||
my $product = Bugzilla::Product->create(\%create_params);
|
||||
|
||||
# Create groups and series for the new product, if requested.
|
||||
$product->_create_bug_group() if $ARGS->{makeproductgroup};
|
||||
my $new_bug_group = $product->_create_bug_group() if $ARGS->{makeproductgroup};
|
||||
if (!$user->in_group('editcomponents') || $ARGS->{makeadmingroup})
|
||||
{
|
||||
# User has no global editcomponents permission, so grant 'createproducts'
|
||||
# group the right to manage his newly created product (or admin group is explicitly requested)
|
||||
$product->_create_bug_group(1, $new_bug_group);
|
||||
}
|
||||
$product->_create_series() if $ARGS->{createseries};
|
||||
|
||||
$product->check_open_product();
|
||||
delete_token($token);
|
||||
|
||||
Bugzilla->dbh->bz_commit_transaction();
|
||||
|
||||
$vars->{message} = 'product_created';
|
||||
$vars->{product} = $product;
|
||||
if ($useclassification)
|
||||
{
|
||||
$vars->{classification} = new Bugzilla::Classification($product->classification_id);
|
||||
$vars->{classifications} = [ Bugzilla::Classification->get_all ];
|
||||
}
|
||||
$vars->{token} = issue_session_token('edit_product');
|
||||
|
||||
|
@ -285,7 +298,7 @@ if ($action eq 'edit' || (!$action && $product_name))
|
|||
}
|
||||
$vars->{product} = $product;
|
||||
$vars->{token} = issue_session_token('edit_product');
|
||||
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
|
||||
$vars->{all_groups} = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
|
||||
|
||||
$template->process('admin/products/edit.html.tmpl', $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
@ -366,8 +379,9 @@ if ($action eq 'updategroupcontrols')
|
|||
my $product = $user->check_can_admin_product($product_name);
|
||||
check_token_data($token, 'edit_group_controls');
|
||||
|
||||
my @now_na = ();
|
||||
my @now_mandatory = ();
|
||||
my @now_na;
|
||||
my @now_mandatory;
|
||||
my @now_entry;
|
||||
my %membercontrol_g;
|
||||
my %othercontrol_g;
|
||||
foreach my $f (keys %$ARGS)
|
||||
|
@ -459,6 +473,9 @@ if ($action eq 'updategroupcontrols')
|
|||
$vars->{product} = $product;
|
||||
$vars->{changes} = $changes;
|
||||
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
|
||||
$template->process('admin/products/groupcontrol/updated.html.tmpl', $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
exit;
|
||||
|
|
|
@ -279,22 +279,12 @@ elsif ($action eq 'update')
|
|||
$changes = $otherUser->update();
|
||||
}
|
||||
|
||||
# Update group settings.
|
||||
my $sth_add_mapping = $dbh->prepare(
|
||||
"INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
my $sth_remove_mapping = $dbh->prepare(
|
||||
"DELETE FROM user_group_map WHERE user_id=? AND group_id=? AND isbless=? AND grant_type=?"
|
||||
);
|
||||
|
||||
my @groupsAddedTo;
|
||||
my @groupsRemovedFrom;
|
||||
my @groupsGrantedRightsToBless;
|
||||
my @groupsDeniedRightsToBless;
|
||||
|
||||
# Regard only groups the user is allowed to bless and skip all others silently.
|
||||
# FIXME: checking for existence of each user_group_map entry would allow to
|
||||
# display a friendlier error message on page reloads.
|
||||
userDataToVars($otherUserID);
|
||||
my $permissions = $vars->{permissions};
|
||||
foreach my $blessable (@{$user->bless_groups()})
|
||||
|
@ -308,13 +298,11 @@ elsif ($action eq 'update')
|
|||
{
|
||||
if (!$groupid)
|
||||
{
|
||||
$sth_remove_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT);
|
||||
push @groupsRemovedFrom, $name;
|
||||
push @groupsRemovedFrom, $blessable;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sth_add_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT);
|
||||
push @groupsAddedTo, $name;
|
||||
push @groupsAddedTo, $blessable;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,27 +315,20 @@ elsif ($action eq 'update')
|
|||
{
|
||||
if (!$groupid)
|
||||
{
|
||||
$sth_remove_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT);
|
||||
push @groupsDeniedRightsToBless, $name;
|
||||
push @groupsDeniedRightsToBless, $blessable;
|
||||
}
|
||||
else
|
||||
{
|
||||
$sth_add_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT);
|
||||
push @groupsGrantedRightsToBless, $name;
|
||||
push @groupsGrantedRightsToBless, $blessable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (@groupsAddedTo || @groupsRemovedFrom)
|
||||
{
|
||||
$dbh->do(
|
||||
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue)".
|
||||
" VALUES (?, ?, NOW(), ?, ?, ?)", undef,
|
||||
$otherUserID, $userid, Bugzilla->get_field('bug_group')->id,
|
||||
join(', ', @groupsRemovedFrom), join(', ', @groupsAddedTo)
|
||||
);
|
||||
}
|
||||
# FIXME: should create profiles_activity entries for blesser changes.
|
||||
|
||||
Bugzilla::Group::add_user_groups([ map { { group => $_, user => $otherUser } } @groupsAddedTo ]);
|
||||
Bugzilla::Group::remove_user_groups([ map { { group => $_, user => $otherUser } } @groupsRemovedFrom ]);
|
||||
Bugzilla::Group::add_user_groups([ map { { group => $_, user => $otherUser } } @groupsGrantedRightsToBless ], 1);
|
||||
Bugzilla::Group::remove_user_groups([ map { { group => $_, user => $otherUser } } @groupsDeniedRightsToBless ], 1);
|
||||
|
||||
$dbh->bz_commit_transaction();
|
||||
|
||||
|
@ -357,13 +338,15 @@ elsif ($action eq 'update')
|
|||
|
||||
Bugzilla::Hook::process('editusers-post_update', { userid => $otherUserID });
|
||||
Bugzilla::Views::refresh_some_views([ $otherUser->login ]);
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
|
||||
$vars->{message} = 'account_updated';
|
||||
$vars->{changed_fields} = [ keys %$changes ];
|
||||
$vars->{groups_added_to} = \@groupsAddedTo;
|
||||
$vars->{groups_removed_from} = \@groupsRemovedFrom;
|
||||
$vars->{groups_granted_rights_to_bless} = \@groupsGrantedRightsToBless;
|
||||
$vars->{groups_denied_rights_to_bless} = \@groupsDeniedRightsToBless;
|
||||
$vars->{groups_added_to} = [ map { $_->name } @groupsAddedTo ];
|
||||
$vars->{groups_removed_from} = [ map { $_->name } @groupsRemovedFrom ];
|
||||
$vars->{groups_granted_rights_to_bless} = [ map { $_->name } @groupsGrantedRightsToBless ];
|
||||
$vars->{groups_denied_rights_to_bless} = [ map { $_->name } @groupsDeniedRightsToBless ];
|
||||
# We already display the updated page. We have to recreate a token now.
|
||||
$vars->{token} = issue_session_token('edit_user');
|
||||
|
||||
|
|
|
@ -54,30 +54,14 @@ if (@add_members || @add_bless || @rm_members || @rm_bless)
|
|||
} };
|
||||
for (\@add_members, \@add_bless)
|
||||
{
|
||||
@$_ = map { $users->{lc $_} ? $users->{lc $_}->id : ThrowUserError('invalid_username', { name => $_ }) } @$_;
|
||||
@$_ = map { $users->{lc $_} || ThrowUserError('invalid_username', { name => $_ }) } @$_;
|
||||
}
|
||||
if (@add_members || @add_bless)
|
||||
{
|
||||
# FIXME Use object method instead of direct DB query
|
||||
Bugzilla->dbh->do(
|
||||
"INSERT IGNORE INTO user_group_map (user_id, group_id, grant_type, isbless) VALUES ".
|
||||
join(', ', ("(?, ?, ?, ?)") x (@add_members + @add_bless)), undef,
|
||||
(map { $_, $vars->{group}->id, GRANT_DIRECT, 0 } @add_members),
|
||||
(map { $_, $vars->{group}->id, GRANT_DIRECT, 1 } @add_bless)
|
||||
);
|
||||
$vars->{group}->add_users(\@add_members, 0);
|
||||
$vars->{group}->add_users(\@add_bless, 1);
|
||||
}
|
||||
}
|
||||
if (@rm_members || @rm_bless)
|
||||
{
|
||||
# FIXME Use object method instead of direct DB query
|
||||
trick_taint($_) for @rm_members, @rm_bless;
|
||||
Bugzilla->dbh->do(
|
||||
"DELETE FROM user_group_map WHERE group_id=? AND grant_type=? AND (user_id, isbless) IN (".
|
||||
join(', ', ("(?, ?)") x (@rm_members + @rm_bless)).")", undef,
|
||||
$vars->{group}->id, GRANT_DIRECT,
|
||||
(map { int($_), 0 } @rm_members), (map { int($_), 1 } @rm_bless)
|
||||
);
|
||||
}
|
||||
$vars->{group}->remove_users(\@rm_members, 0);
|
||||
$vars->{group}->remove_users(\@rm_bless, 1);
|
||||
if (@add_members || @rm_members)
|
||||
{
|
||||
Bugzilla::Hook::process('editusersingroup-post_add', {
|
||||
|
@ -89,6 +73,8 @@ if (@add_members || @add_bless || @rm_members || @rm_bless)
|
|||
if (@add_members || @rm_members)
|
||||
{
|
||||
Bugzilla::Views::refresh_some_views();
|
||||
# Refresh fieldvaluecontrol cache
|
||||
Bugzilla->get_field('delta_ts')->touch;
|
||||
}
|
||||
delete_token($ARGS->{token});
|
||||
my $url = "editusersingroup.cgi?group=".$vars->{group}->id;
|
||||
|
|
|
@ -165,7 +165,7 @@ if ($action eq 'update')
|
|||
}
|
||||
if ($value->field->value_field)
|
||||
{
|
||||
$vars->{changes}->{visibility_values} = $value->set_visibility_values([ list $ARGS->{visibility_value_id} ]);
|
||||
$vars->{changes}->{visibility_values} = $value->set_visibility_values([ list $ARGS->{visibility_value_id} ], 'SKIP_INVISIBLE');
|
||||
}
|
||||
$vars->{changes}->{control_lists} = 1 if $field->update_control_lists($value->id, $ARGS);
|
||||
delete_token($token);
|
||||
|
|
|
@ -247,6 +247,7 @@ if ($ARGS->{update})
|
|||
my $title = $ARGS->{"query_title_$qid"} || '';
|
||||
my $o_onemailperbug = $ARGS->{"orig_query_onemailperbug_$qid"} || 0;
|
||||
my $onemailperbug = $ARGS->{"query_onemailperbug_$qid"} ? 1 : 0;
|
||||
my $isreport = 0;
|
||||
|
||||
if ($o_sort != $sort || $o_queryname ne $queryname ||
|
||||
$o_onemailperbug != $onemailperbug || $o_title ne $title)
|
||||
|
@ -254,9 +255,13 @@ if ($ARGS->{update})
|
|||
detaint_natural($sort);
|
||||
trick_taint($queryname);
|
||||
trick_taint($title);
|
||||
if ($queryname =~ /^([01])-(.*)$/s)
|
||||
{
|
||||
($isreport, $queryname) = ($1, $2);
|
||||
}
|
||||
$dbh->do(
|
||||
"UPDATE whine_queries SET sortkey=?, query_name=?, title=?, onemailperbug=? WHERE id=?",
|
||||
undef, $sort, $queryname, $title, $onemailperbug, $qid
|
||||
"UPDATE whine_queries SET sortkey=?, query_name=?, title=?, onemailperbug=?, isreport=? WHERE id=?",
|
||||
undef, $sort, $queryname, $title, $onemailperbug, $isreport, $qid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -298,11 +303,11 @@ for my $event_id (keys %{$events})
|
|||
my $mailto = '';
|
||||
if ($mailto_type == MAILTO_USER)
|
||||
{
|
||||
$mailto = $schedule->mailto->login;
|
||||
$mailto = $schedule->mailto && $schedule->mailto->login;
|
||||
}
|
||||
elsif ($mailto_type == MAILTO_GROUP)
|
||||
{
|
||||
$mailto = $schedule->mailto->name;
|
||||
$mailto = $schedule->mailto && $schedule->mailto->name;
|
||||
}
|
||||
push @{$events->{$event_id}->{schedule}}, {
|
||||
day => $schedule->run_day,
|
||||
|
@ -323,6 +328,7 @@ for my $event_id (keys %{$events})
|
|||
sort => $query->sortkey,
|
||||
id => $query->id,
|
||||
onemailperbug => $query->one_email_per_bug,
|
||||
isreport => $query->isreport,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -330,7 +336,8 @@ for my $event_id (keys %{$events})
|
|||
$vars->{events} = $events;
|
||||
|
||||
# get the available queries
|
||||
$vars->{available_queries} = $dbh->selectcol_arrayref("SELECT name FROM namedqueries WHERE userid=?", undef, $userid) || [];
|
||||
$vars->{available_queries} = $dbh->selectcol_arrayref("SELECT name FROM namedqueries WHERE userid=? ORDER BY name", undef, $userid) || [];
|
||||
$vars->{available_reports} = $dbh->selectcol_arrayref("SELECT name FROM reports WHERE user_id=? ORDER BY name", undef, $userid) || [];
|
||||
$vars->{token} = issue_session_token('edit_whine');
|
||||
$vars->{local_timezone} = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now());
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/perl -wT
|
||||
# HTTP handler for incoming e-mail
|
||||
|
||||
use strict;
|
||||
use lib qw(. lib);
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::InMail;
|
||||
|
||||
my $status;
|
||||
if (!Bugzilla->params->{enable_inmail_cgi})
|
||||
{
|
||||
$status = 'disabled';
|
||||
}
|
||||
else
|
||||
{
|
||||
my $mail_text = Bugzilla->cgi->param('POSTDATA');
|
||||
if (!$mail_text)
|
||||
{
|
||||
$status = 'empty-message';
|
||||
}
|
||||
else
|
||||
{
|
||||
$status = Bugzilla::InMail::process_inmail($mail_text) == 1 ? 'success' : 'error';
|
||||
}
|
||||
}
|
||||
|
||||
Bugzilla->cgi->send_header('application/json');
|
||||
print '{"status":"'.$status.'"}';
|
||||
exit;
|
||||
|
||||
__END__
|
||||
|
||||
Postfix configuration example:
|
||||
|
||||
1) If you want to log all incoming messages, create /etc/postfix/send-to-bugzilla script with the following content:
|
||||
|
||||
#!/bin/sh
|
||||
echo '-----' >> /var/log/bugzilla-email-in.log
|
||||
/usr/bin/tee -a /var/log/bugzilla-email-in.log | curl -X POST -H 'Content-Type: text/plain' --data-binary @- http://127.0.0.1:8157/email_in.cgi
|
||||
|
||||
2) If you don't want to log all incoming messages, create /etc/postfix/send-to-bugzilla script with the following content:
|
||||
|
||||
#!/bin/sh
|
||||
curl -X POST -H 'Content-Type: text/plain' --data-binary @- http://127.0.0.1:8157/email_in.cgi
|
||||
|
||||
3) Make it executable:
|
||||
|
||||
chmod 755 /etc/postfix/send-to-bugzilla
|
||||
|
||||
4) Add the following to master.cf:
|
||||
|
||||
bugzilla unix - n n - - pipe
|
||||
flags=DRhu user=www-data:www-data argv=/etc/postfix/send-to-bugzilla
|
||||
|
||||
5) Add the following to your /etc/postfix/transport map:
|
||||
|
||||
daemon@your.bugzilla.url bugzilla:
|
||||
|
||||
Where `daemon@your.bugzilla.url` is the same as `mailfrom` Bugzilla parameter from Administration -> Config
|
||||
This will make your Postfix feed all messages sent to `daemon@your.bugzilla.url` to email_in.cgi.
|
||||
|
||||
6) Run `postmap /etc/postfix/transport`
|
||||
|
||||
7) Ensure that other parts of your Postfix configuration do not prevent it from receiving mail to daemon@your.bugzilla.url
|
||||
|
||||
8) Turn `enable_inmail_cgi` parameter on in Administration -> Config
|
||||
|
||||
9) Deny access to `email_in.cgi` in your HTTP server. For example with nginx:
|
||||
|
||||
location /email_in.cgi {
|
||||
deny all;
|
||||
}
|
489
email_in.pl
489
email_in.pl
|
@ -30,430 +30,16 @@ BEGIN
|
|||
my ($a) = abs_path($0) =~ /^(.*)$/;
|
||||
chdir dirname($a);
|
||||
}
|
||||
|
||||
use lib qw(. lib);
|
||||
use Bugzilla::InMail;
|
||||
|
||||
use Data::Dumper;
|
||||
use Email::Address;
|
||||
use Email::Reply qw(reply);
|
||||
use Email::MIME;
|
||||
use Email::MIME::Attachment::Stripper;
|
||||
use HTML::Strip;
|
||||
use Getopt::Long qw(:config bundling);
|
||||
use Pod::Usage;
|
||||
use Encode;
|
||||
use Scalar::Util qw(blessed);
|
||||
my $switch = {};
|
||||
|
||||
use Bugzilla;
|
||||
use Bugzilla::Attachment;
|
||||
use Bugzilla::Bug;
|
||||
use Bugzilla::Hook;
|
||||
use Bugzilla::BugMail;
|
||||
use Bugzilla::Constants;
|
||||
use Bugzilla::Error;
|
||||
use Bugzilla::Mailer;
|
||||
use Bugzilla::Token;
|
||||
use Bugzilla::User;
|
||||
use Bugzilla::Util;
|
||||
|
||||
#############
|
||||
# Constants #
|
||||
#############
|
||||
|
||||
# This is the USENET standard line for beginning a signature block
|
||||
# in a message. RFC-compliant mailers use this.
|
||||
use constant SIGNATURE_DELIMITER => '-- ';
|
||||
|
||||
# $input_email is a global so that it can be used in die_handler.
|
||||
our ($input_email, %switch);
|
||||
|
||||
####################
|
||||
# Main Subroutines #
|
||||
####################
|
||||
|
||||
sub parse_mail
|
||||
{
|
||||
my ($mail_text) = @_;
|
||||
debug_print('Parsing Email');
|
||||
$input_email = Email::MIME->new($mail_text);
|
||||
|
||||
my %fields;
|
||||
Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, fields => \%fields });
|
||||
# RFC 3834 - Recommendations for Automatic Responses to Electronic Mail
|
||||
# Automatic responses SHOULD NOT be issued in response to any
|
||||
# message which contains an Auto-Submitted header field (see below),
|
||||
# where that field has any value other than "no".
|
||||
# F*cking MS Exchange sometimes does not append Auto-Submitted header
|
||||
# to delivery status reports, so also check content-type.
|
||||
my $autosubmitted;
|
||||
if (lc($input_email->header('Auto-Submitted') || 'no') ne 'no' ||
|
||||
($input_email->header('X-Auto-Response-Suppress') || '') =~ /all/iso ||
|
||||
($input_email->header('Content-Type') || '') =~ /delivery-status/iso)
|
||||
{
|
||||
debug_print("Rejecting email with Auto-Submitted = $autosubmitted");
|
||||
exit 0;
|
||||
}
|
||||
|
||||
my $dbh = Bugzilla->dbh;
|
||||
|
||||
# Fetch field => value from emailin_fields table
|
||||
my ($toemail) = Email::Address->parse($input_email->header('To'));
|
||||
%fields = (%fields, map { @$_ } @{ $dbh->selectall_arrayref(
|
||||
"SELECT field, value FROM emailin_fields WHERE address=?",
|
||||
undef, $toemail) || [] });
|
||||
|
||||
my $summary = $input_email->header('Subject');
|
||||
if ($summary =~ /\[\s*Bug\s*(\d+)\s*\](.*)/i)
|
||||
{
|
||||
$fields{bug_id} = $1;
|
||||
$summary = trim($2);
|
||||
}
|
||||
$fields{_subject} = $summary;
|
||||
|
||||
# Add CC's from email Cc: header
|
||||
$fields{newcc} = $input_email->header('Cc');
|
||||
$fields{newcc} = $fields{newcc} && (join ', ', map { [ Email::Address->parse($_) ] -> [0] }
|
||||
split /\s*,\s*/, $fields{newcc}) || undef;
|
||||
|
||||
my ($body, $attachments) = get_body_and_attachments($input_email);
|
||||
if (@$attachments)
|
||||
{
|
||||
$fields{attachments} = $attachments;
|
||||
}
|
||||
|
||||
debug_print("Body:\n" . $body, 3);
|
||||
|
||||
$body = remove_leading_blank_lines($body);
|
||||
|
||||
Bugzilla::Hook::process("emailin-filter_body", { body => \$body });
|
||||
|
||||
my @body_lines = split(/\r?\n/s, $body);
|
||||
my $fields_by_name = { map { (lc($_->description) => $_->name, lc($_->name) => $_->name) } Bugzilla->get_fields({ obsolete => 0 }) };
|
||||
|
||||
# If there are fields specified.
|
||||
if ($body =~ /^\s*@/s)
|
||||
{
|
||||
my $current_field;
|
||||
while (my $line = shift @body_lines)
|
||||
{
|
||||
# If the sig is starting, we want to keep this in the
|
||||
# @body_lines so that we don't keep the sig as part of the
|
||||
# comment down below.
|
||||
if ($line eq SIGNATURE_DELIMITER)
|
||||
{
|
||||
unshift(@body_lines, $line);
|
||||
last;
|
||||
}
|
||||
# Otherwise, we stop parsing fields on the first blank line.
|
||||
$line = trim($line);
|
||||
last if !$line;
|
||||
if ($line =~ /^\@\s*(.+?)\s*=\s*(.*)\s*/)
|
||||
{
|
||||
$current_field = $fields_by_name->{lc($1)} || lc($1);
|
||||
$fields{$current_field} = $2;
|
||||
}
|
||||
else
|
||||
{
|
||||
$fields{$current_field} .= " $line";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
%fields = %{ Bugzilla::Bug::map_fields(\%fields) };
|
||||
|
||||
my ($reporter) = Email::Address->parse($input_email->header('From'));
|
||||
$fields{reporter} = $reporter->address;
|
||||
|
||||
{
|
||||
my $r;
|
||||
if ($r = $reporter->phrase)
|
||||
{
|
||||
$r .= ' ' . $reporter->comment if $reporter->comment;
|
||||
}
|
||||
else
|
||||
{
|
||||
$r = $reporter->address;
|
||||
}
|
||||
$fields{_reporter_name} = $r;
|
||||
}
|
||||
|
||||
# The summary line only affects us if we're doing a post_bug.
|
||||
# We have to check it down here because there might have been
|
||||
# a bug_id specified in the body of the email.
|
||||
if (!$fields{bug_id} && !$fields{short_desc})
|
||||
{
|
||||
$fields{short_desc} = $summary;
|
||||
}
|
||||
|
||||
my $comment = '';
|
||||
# Get the description, except the signature.
|
||||
foreach my $line (@body_lines)
|
||||
{
|
||||
last if $line eq SIGNATURE_DELIMITER;
|
||||
$comment .= "$line\n";
|
||||
}
|
||||
$fields{comment} = $comment;
|
||||
|
||||
debug_print("Parsed Fields:\n" . Dumper(\%fields), 2);
|
||||
|
||||
return \%fields;
|
||||
}
|
||||
|
||||
sub post_bug
|
||||
{
|
||||
my ($fields) = @_;
|
||||
debug_print('Posting a new bug...');
|
||||
my $bug;
|
||||
$Bugzilla::Error::IN_EVAL++;
|
||||
eval
|
||||
{
|
||||
my ($retval, $non_conclusive_fields) =
|
||||
Bugzilla::User::match_field({
|
||||
assigned_to => { type => 'single' },
|
||||
qa_contact => { type => 'single' },
|
||||
cc => { type => 'multi' }
|
||||
}, $fields, MATCH_SKIP_CONFIRM);
|
||||
if ($retval != USER_MATCH_SUCCESS)
|
||||
{
|
||||
ThrowUserError('user_match_too_many', { fields => $non_conclusive_fields });
|
||||
}
|
||||
$bug = Bugzilla::Bug::create_or_update($fields);
|
||||
};
|
||||
$Bugzilla::Error::IN_EVAL--;
|
||||
if (my $err = $@)
|
||||
{
|
||||
my $format = "\n\nIncoming mail format for entering bugs:\n\n\@field = value\n\@field = value\n...\n\n<Bug description...>\n";
|
||||
if (blessed $err && $err->{message})
|
||||
{
|
||||
$err->{message} .= $format;
|
||||
}
|
||||
else
|
||||
{
|
||||
$err .= $format;
|
||||
}
|
||||
die $err;
|
||||
}
|
||||
if ($bug)
|
||||
{
|
||||
debug_print("Created bug " . $bug->id);
|
||||
return ($bug, $bug->comments->[0]);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub handle_attachments
|
||||
{
|
||||
my ($bug, $attachments, $comment) = @_;
|
||||
return if !$attachments;
|
||||
debug_print("Handling attachments...");
|
||||
my $dbh = Bugzilla->dbh;
|
||||
$dbh->bz_start_transaction();
|
||||
my ($update_comment, $update_bug);
|
||||
foreach my $attachment (@$attachments)
|
||||
{
|
||||
my $data = delete $attachment->{payload};
|
||||
debug_print("Inserting Attachment: " . Dumper($attachment), 2);
|
||||
$attachment->{content_type} ||= 'application/octet-stream';
|
||||
my $obj = Bugzilla::Attachment->create({
|
||||
bug => $bug,
|
||||
description => $attachment->{filename},
|
||||
filename => $attachment->{filename},
|
||||
mimetype => $attachment->{content_type},
|
||||
data => $data,
|
||||
});
|
||||
# If we added a comment, and our comment does not already have a type,
|
||||
# and this is our first attachment, then we make the comment an
|
||||
# "attachment created" comment.
|
||||
if ($comment and !$comment->type and !$update_comment)
|
||||
{
|
||||
$comment->set_type(CMT_ATTACHMENT_CREATED, $obj->id);
|
||||
$update_comment = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$bug->add_comment('', { type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id });
|
||||
$update_bug = 1;
|
||||
}
|
||||
}
|
||||
# We only update the comments and bugs at the end of the transaction,
|
||||
# because doing so modifies bugs_fulltext, which is a non-transactional
|
||||
# table.
|
||||
$bug->update() if $update_bug;
|
||||
$comment->update() if $update_comment;
|
||||
$dbh->bz_commit_transaction();
|
||||
}
|
||||
|
||||
######################
|
||||
# Helper Subroutines #
|
||||
######################
|
||||
|
||||
sub debug_print
|
||||
{
|
||||
my ($str, $level) = @_;
|
||||
$level ||= 1;
|
||||
print STDERR "$str\n" if $level <= $switch{verbose};
|
||||
}
|
||||
|
||||
sub get_body_and_attachments
|
||||
{
|
||||
my ($email) = @_;
|
||||
|
||||
my $ct = $email->content_type || 'text/plain';
|
||||
debug_print("Splitting Body and Attachments [Type: $ct]...");
|
||||
|
||||
my $body;
|
||||
my $attachments = [];
|
||||
if ($ct =~ /^multipart\/(alternative|signed)/i)
|
||||
{
|
||||
$body = get_text_alternative($email);
|
||||
}
|
||||
else
|
||||
{
|
||||
my $stripper = new Email::MIME::Attachment::Stripper($email, force_filename => 1);
|
||||
my $message = $stripper->message;
|
||||
$body = get_text_alternative($message);
|
||||
$attachments = [$stripper->attachments];
|
||||
}
|
||||
$email->charset_set('utf8');
|
||||
$email->body_str_set($body);
|
||||
|
||||
return ($body, $attachments);
|
||||
}
|
||||
|
||||
sub rm_line_feeds
|
||||
{
|
||||
my ($t) = @_;
|
||||
$t =~ s/[\n\r]+/ /giso;
|
||||
return $t;
|
||||
}
|
||||
|
||||
sub get_text_alternative
|
||||
{
|
||||
my ($email) = @_;
|
||||
|
||||
my @parts = $email->parts;
|
||||
my $body;
|
||||
foreach my $part (@parts)
|
||||
{
|
||||
my $ct = $part->content_type || 'text/plain';
|
||||
my $charset = 'iso-8859-1';
|
||||
# The charset may be quoted.
|
||||
if ($ct =~ /charset="?([^;"]+)/)
|
||||
{
|
||||
$charset = $1;
|
||||
}
|
||||
debug_print("Part Content-Type: $ct", 2);
|
||||
debug_print("Part Character Encoding: $charset", 2);
|
||||
if (!$ct || $ct =~ /^text\/plain/i)
|
||||
{
|
||||
$body = $part->body;
|
||||
}
|
||||
elsif ($ct =~ /^text\/html/i)
|
||||
{
|
||||
$body = $part->body;
|
||||
$body =~ s/<table[^<>]*class=[\"\']?difft[^<>]*>.*?<\/table\s*>//giso;
|
||||
$body =~ s/(<a[^<>]*>.*?<\/a\s*>)/rm_line_feeds($1)/gieso;
|
||||
Bugzilla::Hook::process("emailin-filter_html", { body => \$body });
|
||||
$body = HTML::Strip->new->parse($body);
|
||||
}
|
||||
if (defined $body)
|
||||
{
|
||||
if (Bugzilla->params->{utf8} && !utf8::is_utf8($body))
|
||||
{
|
||||
$body = Encode::decode($charset, $body);
|
||||
}
|
||||
last;
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined $body)
|
||||
{
|
||||
# Note that this only happens if the email does not contain any
|
||||
# text/plain parts. If the email has an empty text/plain part,
|
||||
# you're fine, and this message does NOT get thrown.
|
||||
ThrowUserError('email_no_text_plain');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
sub remove_leading_blank_lines
|
||||
{
|
||||
my ($text) = @_;
|
||||
$text =~ s/^(\s*\n)+//s;
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub die_handler
|
||||
{
|
||||
my ($msg) = @_;
|
||||
|
||||
# In Template-Toolkit, [% RETURN %] is implemented as a call to "die".
|
||||
# But of course, we really don't want to actually *die* just because
|
||||
# the user-error or code-error template ended. So we don't really die.
|
||||
return if blessed($msg) && $msg->isa('Template::Exception') && $msg->type eq 'return';
|
||||
|
||||
# If this is inside an eval, then we should just act like...we're
|
||||
# in an eval (instead of printing the error and exiting).
|
||||
die(@_) if $^S;
|
||||
|
||||
if (ref $msg eq 'Bugzilla::Error')
|
||||
{
|
||||
$msg = $msg->{message};
|
||||
}
|
||||
|
||||
# We can't depend on the MTA to send an error message, so we have
|
||||
# to generate one properly.
|
||||
if ($input_email)
|
||||
{
|
||||
my $from = Bugzilla->params->{mailfrom};
|
||||
my $reply = reply(to => $input_email, from => $from, top_post => 1, body => "$msg\n");
|
||||
MessageToMTA($reply->as_string);
|
||||
}
|
||||
print STDERR "$msg\n";
|
||||
# We exit with a successful value, because we don't want the MTA
|
||||
# to *also* send a failure notice.
|
||||
exit;
|
||||
}
|
||||
|
||||
# Use UTF-8 in Email::Reply to correctly quote the body
|
||||
my $crlf = "\x0d\x0a";
|
||||
my $CRLF = $crlf;
|
||||
undef *Email::Reply::_quote_body;
|
||||
*Email::Reply::_quote_body = sub
|
||||
{
|
||||
my ($self, $part) = @_;
|
||||
return if length $self->{quoted};
|
||||
return map $self->_quote_body($_), $part->parts if $part->parts > 1;
|
||||
return if $part->content_type && $part->content_type !~ m[\btext/plain\b];
|
||||
|
||||
my $body = $part->body;
|
||||
Encode::_utf8_on($body);
|
||||
|
||||
$body = ($self->_strip_sig($body) || $body)
|
||||
if !$self->{keep_sig} && $body =~ /$crlf--\s*$crlf/o;
|
||||
|
||||
my ($end) = $body =~ /($crlf)/;
|
||||
$end ||= $CRLF;
|
||||
$body =~ s/[\r\n\s]+$//;
|
||||
$body = $self->_quote_orig_body($body);
|
||||
$body = "$self->{attrib}$end$body$end";
|
||||
|
||||
$self->{crlf} = $end;
|
||||
$self->{quoted} = $body;
|
||||
};
|
||||
|
||||
###############
|
||||
# Main Script #
|
||||
###############
|
||||
|
||||
$SIG{__DIE__} = \&die_handler;
|
||||
|
||||
GetOptions(\%switch, 'help|h', 'verbose|v+');
|
||||
$switch{verbose} ||= 0;
|
||||
GetOptions($switch, 'help|h', 'verbose|v+');
|
||||
$switch->{verbose} ||= 0;
|
||||
|
||||
# Print the help message if that switch was selected.
|
||||
pod2usage({-verbose => 0, -exitval => 1}) if $switch{help};
|
||||
pod2usage({-verbose => 0, -exitval => 1}) if $switch->{help};
|
||||
|
||||
# Get a next-in-pipe command from commandline
|
||||
my ($pipe) = join(' ', @ARGV) =~ /^(.*)$/iso;
|
||||
|
@ -472,69 +58,8 @@ if ($pipe && open PIPE, "| $pipe")
|
|||
close PIPE;
|
||||
}
|
||||
|
||||
my $mail_fields = parse_mail($mail_text);
|
||||
|
||||
Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields });
|
||||
|
||||
my $attachments = delete $mail_fields->{attachments};
|
||||
|
||||
my $username = $mail_fields->{reporter};
|
||||
# If emailsuffix is in use, we have to remove it from the email address.
|
||||
if (my $suffix = Bugzilla->params->{emailsuffix})
|
||||
{
|
||||
$username =~ s/\Q$suffix\E$//i;
|
||||
}
|
||||
|
||||
# First try to select user with name $username
|
||||
my $user = Bugzilla::User->new({ name => $username });
|
||||
|
||||
# Then try to find alias $username for some user
|
||||
unless ($user)
|
||||
{
|
||||
my $dbh = Bugzilla->dbh;
|
||||
($user) = $dbh->selectrow_array("SELECT userid FROM emailin_aliases WHERE address=?", undef, trim($mail_fields->{reporter}));
|
||||
$user = Bugzilla::User->new({ id => $user }) if $user;
|
||||
# Then check if autoregistration is enabled
|
||||
unless ($user)
|
||||
{
|
||||
unless (Bugzilla->params->{emailin_autoregister})
|
||||
{
|
||||
ThrowUserError('invalid_username', { name => $username });
|
||||
}
|
||||
# Then try to autoregister unknown user
|
||||
$user = Bugzilla::User->create({
|
||||
login_name => $username,
|
||||
realname => $mail_fields->{_reporter_name},
|
||||
cryptpassword => 'a3#',
|
||||
disabledtext => '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user->is_enabled)
|
||||
{
|
||||
ThrowUserError('account_disabled', { disabled_reason => $user->disabledtext });
|
||||
}
|
||||
|
||||
Bugzilla->set_user($user);
|
||||
|
||||
my ($bug, $comment);
|
||||
if ($mail_fields->{bug_id})
|
||||
{
|
||||
debug_print("Updating Bug $mail_fields->{bug_id}...");
|
||||
$bug = Bugzilla::Bug::create_or_update($mail_fields);
|
||||
$comment = $bug->comments->[-1] if trim($mail_fields->{comment});
|
||||
}
|
||||
else
|
||||
{
|
||||
($bug, $comment) = post_bug($mail_fields);
|
||||
}
|
||||
|
||||
handle_attachments($bug, $attachments, $comment);
|
||||
|
||||
Bugzilla->send_mail;
|
||||
|
||||
debug_print("Sent bugmail");
|
||||
Bugzilla::InMail::process_inmail($mail_text);
|
||||
exit;
|
||||
|
||||
__END__
|
||||
|
||||
|
|
|
@ -25,13 +25,10 @@ use strict;
|
|||
use Bugzilla::Extension;
|
||||
|
||||
my $VERSION = '1.0';
|
||||
my $REQUIRED_MODULES = [
|
||||
{
|
||||
my $REQUIRED_MODULES = [ {
|
||||
package => 'PerlMagick',
|
||||
module => 'Image::Magick',
|
||||
version => 0,
|
||||
},
|
||||
];
|
||||
} ];
|
||||
|
||||
extension_version('BmpConvert', $VERSION);
|
||||
required_modules('BmpConvert', $REQUIRED_MODULES);
|
||||
|
|
41
js/bug.js
41
js/bug.js
|
@ -94,8 +94,10 @@ function addCollapseLink(id)
|
|||
function addReplyLink(num, id)
|
||||
{
|
||||
var e = document.getElementById('comment_act_'+id);
|
||||
|
||||
if (!e)
|
||||
return;
|
||||
|
||||
var s = '[';
|
||||
if (user_settings.quote_replies != 'off')
|
||||
{
|
||||
|
@ -104,11 +106,16 @@ function addReplyLink(num, id)
|
|||
}
|
||||
s += ', clone to <a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&cloned_comment='+num+'">other</a>';
|
||||
s += '/<a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&product='+encodeURIComponent(bug_info.product)+'&cloned_comment='+num+'">same</a>';
|
||||
|
||||
// 4Intranet Bug 69514 - Clone to external product button
|
||||
if (bug_info.extprod)
|
||||
s += '/<a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&product='+encodeURIComponent(bug_info.extprod)+'&cloned_comment='+num+'">ext</a>';
|
||||
else if (bug_info.intprod)
|
||||
s += '/<a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&product='+encodeURIComponent(bug_info.intprod)+'&cloned_comment='+num+'">int</a>';
|
||||
|
||||
if (window.bugLinkHook)
|
||||
s += bugLinkHook(num, id);
|
||||
|
||||
s += ' product]';
|
||||
e.innerHTML += s;
|
||||
}
|
||||
|
@ -257,7 +264,9 @@ function updateRemainingTime()
|
|||
function changeform_onsubmit()
|
||||
{
|
||||
if (check_new_keywords(document.changeform) == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var wtInput = document.changeform.work_time;
|
||||
if (!wtInput)
|
||||
|
@ -280,9 +289,21 @@ function changeform_onsubmit()
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (awt != 0)
|
||||
{
|
||||
var txt = document.getElementById('comment_textarea').value.trim();
|
||||
if (txt === '')
|
||||
{
|
||||
var wtonly = document.getElementById('cmt_worktime').checked;
|
||||
if (!wtonly)
|
||||
{
|
||||
alert('You have to specify a comment on this change');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wtInput.value = awt;
|
||||
adjustRemainingTime();
|
||||
window.checkCommentOnUnload = false;
|
||||
return true;
|
||||
}
|
||||
|
@ -493,10 +514,20 @@ function to_attachment_page(link)
|
|||
var form = document.createElement('form');
|
||||
form.action = link.href;
|
||||
form.method = 'post';
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.name = "comment";
|
||||
textarea.value = document.getElementById('comment_textarea').value;
|
||||
form.appendChild(textarea);
|
||||
var e = document.createElement('input');
|
||||
e.type = 'hidden';
|
||||
e.name = 'comment';
|
||||
e.value = document.getElementById('comment_textarea').value;
|
||||
form.appendChild(e);
|
||||
var w = document.getElementById('work_time');
|
||||
if (w)
|
||||
{
|
||||
e = document.createElement('input');
|
||||
e.type = 'hidden';
|
||||
e.name = 'work_time';
|
||||
e.value = w.value;
|
||||
form.appendChild(e);
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
return false;
|
||||
|
|
|
@ -144,7 +144,7 @@ function userAutocomplete(hint, emptyOptions, loadAllOnEmpty)
|
|||
}
|
||||
|
||||
var u = window.location.href.replace(/[^\/]+$/, '');
|
||||
u += 'xml.cgi?method=User.get&output=json&excludedisabled=1&maxusermatches=';
|
||||
u += 'xml.cgi?method=User.get&output=json&excludedisabled=1&include_fields=id&include_fields=real_name&include_fields=email&maxusermatches=';
|
||||
if (hint.input.value)
|
||||
{
|
||||
u += '20';
|
||||
|
|
|
@ -47,7 +47,7 @@ onDomReady(function()
|
|||
addListener(s, 'mouseout', function(e) {
|
||||
e = e || window.event;
|
||||
var t = e.relatedTarget || e.toElement;
|
||||
if (t == s || t.parentNode == s)
|
||||
if (t == s || t && t.parentNode == s)
|
||||
return;
|
||||
s.style.width = lim+'px';
|
||||
s.style.maxWidth = '';
|
||||
|
|
10
js/util.js
10
js/util.js
|
@ -33,6 +33,16 @@ function htmlspecialchars(s)
|
|||
return s;
|
||||
}
|
||||
|
||||
window.http_build_query = function(data)
|
||||
{
|
||||
var encoded = '';
|
||||
for (var i in data)
|
||||
{
|
||||
encoded = encoded+'&'+encodeURIComponent(i)+'='+(data[i] === false || data[i] === null ? '' : encodeURIComponent(data[i]));
|
||||
}
|
||||
return encoded.substr(1);
|
||||
};
|
||||
|
||||
// Checks if a specified value 'val' is in the specified array 'arr'
|
||||
function bz_isValueInArray(arr, val)
|
||||
{
|
||||
|
|
|
@ -221,7 +221,7 @@ if ($ARGS->{delta_ts})
|
|||
|
||||
$vars->{title_tag} = "mid_air";
|
||||
|
||||
ThrowCodeError('undefined_field', { field => 'longdesclength' }) if !$ARGS->{longdesclength};
|
||||
ThrowCodeError('undefined_field', { field => 'longdesclength' }) if !defined $ARGS->{longdesclength};
|
||||
|
||||
$vars->{start_at} = $ARGS->{longdesclength};
|
||||
# Always sort midair collision comments oldest to newest,
|
||||
|
|
|
@ -179,7 +179,7 @@ $vars->{chfield} = [
|
|||
# Fields for reports
|
||||
$vars->{report_columns} = [
|
||||
sort { $a->{sortkey} <=> $b->{sortkey} || $a->{title} cmp $b->{title} }
|
||||
values %{Bugzilla::Search::REPORT_COLUMNS()}
|
||||
values %{Bugzilla::Search->REPORT_COLUMNS()}
|
||||
];
|
||||
|
||||
# Boolean charts
|
||||
|
|
260
report.cgi
260
report.cgi
|
@ -34,7 +34,6 @@ use Bugzilla::Token;
|
|||
my $ARGS = Bugzilla->input_params;
|
||||
my $template = Bugzilla->template;
|
||||
my $vars = {};
|
||||
my $buffer = http_build_query($ARGS);
|
||||
|
||||
# Go straight back to query.cgi if we are adding a boolean chart.
|
||||
if (grep /^cmd-/, keys %$ARGS)
|
||||
|
@ -52,7 +51,8 @@ my $dbh = Bugzilla->switch_to_shadow_db();
|
|||
my $action = $ARGS->{action} || 'menu';
|
||||
my $token = $ARGS->{token};
|
||||
|
||||
if ($action eq "menu")
|
||||
$vars->{measure_descs} = Bugzilla::Report->get_measures();
|
||||
if ($action eq 'menu')
|
||||
{
|
||||
# No need to do any searching in this case, so bail out early.
|
||||
$template->process("reports/menu.html.tmpl", $vars)
|
||||
|
@ -104,210 +104,17 @@ elsif ($action eq 'del')
|
|||
exit;
|
||||
}
|
||||
|
||||
my $valid_columns = Bugzilla::Search::REPORT_COLUMNS();
|
||||
$vars->{report_columns} = $valid_columns;
|
||||
|
||||
my $field = {};
|
||||
for (qw(x y z))
|
||||
{
|
||||
my $f = $ARGS->{$_.'_axis_field'} || '';
|
||||
trick_taint($f);
|
||||
if ($f)
|
||||
{
|
||||
if ($valid_columns->{$f})
|
||||
{
|
||||
$field->{$_} = $f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThrowCodeError("report_axis_invalid", {fld => $_, val => $f});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!keys %$field)
|
||||
{
|
||||
ThrowUserError("no_axes_defined");
|
||||
}
|
||||
|
||||
my $width = $ARGS->{width};
|
||||
my $height = $ARGS->{height};
|
||||
|
||||
if (defined($width))
|
||||
{
|
||||
(detaint_natural($width) && $width > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
$width <= 2000 || ThrowUserError("chart_too_large");
|
||||
}
|
||||
|
||||
if (defined($height))
|
||||
{
|
||||
(detaint_natural($height) && $height > 0)
|
||||
|| ThrowCodeError("invalid_dimensions");
|
||||
$height <= 2000 || ThrowUserError("chart_too_large");
|
||||
}
|
||||
|
||||
# These shenanigans are necessary to make sure that both vertical and
|
||||
# horizontal 1D tables convert to the correct dimension when you ask to
|
||||
# display them as some sort of chart.
|
||||
my $is_table;
|
||||
if ($ARGS->{format} eq 'table' || $ARGS->{format} eq 'simple')
|
||||
{
|
||||
$is_table = 1;
|
||||
if ($field->{x} && !$field->{y})
|
||||
{
|
||||
# 1D *tables* should be displayed vertically (with a row_field only)
|
||||
$field->{y} = $field->{x};
|
||||
delete $field->{x};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Bugzilla->feature('graphical_reports'))
|
||||
{
|
||||
ThrowCodeError('feature_disabled', { feature => 'graphical_reports' });
|
||||
}
|
||||
if ($field->{y} && !$field->{x})
|
||||
{
|
||||
# 1D *charts* should be displayed horizontally (with an col_field only)
|
||||
$field->{x} = $field->{y};
|
||||
delete $field->{y};
|
||||
}
|
||||
}
|
||||
|
||||
my $measures = {
|
||||
etime => 'estimated_time',
|
||||
rtime => 'remaining_time',
|
||||
wtime => 'interval_time',
|
||||
count => '_count',
|
||||
};
|
||||
# Trick Bugzilla::Search: replace report columns SQL + add '_count' column
|
||||
# FIXME: Remove usage of global variable COLUMNS in search generation code
|
||||
%{Bugzilla::Search->COLUMNS} = (%{Bugzilla::Search->COLUMNS}, %{Bugzilla::Search->REPORT_COLUMNS});
|
||||
Bugzilla::Search->COLUMNS->{_count}->{name} = '1';
|
||||
|
||||
my $measure = $ARGS->{measure};
|
||||
if ($measure eq 'times' ? !$is_table : !$measures->{$measure})
|
||||
{
|
||||
$measure = 'count';
|
||||
}
|
||||
$vars->{measure} = $measure;
|
||||
|
||||
# Validate the values in the axis fields or throw an error.
|
||||
my %a;
|
||||
my @group_by = grep { !($a{$_}++) } values %$field;
|
||||
my @axis_fields = @group_by;
|
||||
for ($measure eq 'times' ? qw(etime rtime wtime) : $measure)
|
||||
{
|
||||
push @axis_fields, $measures->{$_} unless $a{$measures->{$_}};
|
||||
}
|
||||
|
||||
# Clone the params, so that Bugzilla::Search can modify them
|
||||
my $search = new Bugzilla::Search(
|
||||
'fields' => \@axis_fields,
|
||||
'params' => { %{ Bugzilla->input_params } },
|
||||
);
|
||||
my $query = $search->getSQL();
|
||||
$query =
|
||||
"SELECT ".
|
||||
($field->{x} || "''")." x, ".
|
||||
($field->{y} || "''")." y, ".
|
||||
($field->{z} || "''")." z, ".
|
||||
join(', ', map { "SUM($measures->{$_}) $_" } $measure eq 'times' ? qw(etime rtime wtime) : $measure).
|
||||
" FROM ($query) _report_table GROUP BY ".join(", ", @group_by);
|
||||
|
||||
$::SIG{TERM} = 'DEFAULT';
|
||||
$::SIG{PIPE} = 'DEFAULT';
|
||||
|
||||
my $results = $dbh->selectall_arrayref($query, {Slice=>{}});
|
||||
|
||||
# We have a hash of hashes for the data itself, and a hash to hold the
|
||||
# row/col/table names.
|
||||
my %data;
|
||||
my %names;
|
||||
|
||||
# Read the bug data and count the bugs for each possible value of row, column
|
||||
# and table.
|
||||
#
|
||||
# We detect a numerical field, and sort appropriately, if all the values are
|
||||
# numeric.
|
||||
my %isnumeric;
|
||||
|
||||
foreach my $group (@$results)
|
||||
{
|
||||
for (qw(x y z))
|
||||
{
|
||||
$isnumeric{$_} &&= ($group->{$_} =~ /^-?\d+(\.\d+)?$/o);
|
||||
$names{$_}{$group->{$_}} = 1;
|
||||
}
|
||||
$data{$group->{z}}{$group->{x}}{$group->{y}} = $is_table ? $group : $group->{$measure};
|
||||
}
|
||||
|
||||
my @tbl_names = @{get_names($names{z}, $isnumeric{z}, $field->{z})};
|
||||
my @col_names = @{get_names($names{x}, $isnumeric{x}, $field->{x})};
|
||||
my @row_names = @{get_names($names{y}, $isnumeric{y}, $field->{y})};
|
||||
|
||||
# The GD::Graph package requires a particular format of data, so once we've
|
||||
# gathered everything into the hashes and made sure we know the size of the
|
||||
# data, we reformat it into an array of arrays of arrays of data.
|
||||
push @tbl_names, "-total-" if scalar(@tbl_names) > 1;
|
||||
|
||||
my @image_data;
|
||||
foreach my $tbl (@tbl_names)
|
||||
{
|
||||
my @tbl_data;
|
||||
push @tbl_data, \@col_names;
|
||||
foreach my $row (@row_names)
|
||||
{
|
||||
my @col_data;
|
||||
foreach my $col (@col_names)
|
||||
{
|
||||
$data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
|
||||
push @col_data, $data{$tbl}{$col}{$row};
|
||||
if ($tbl ne "-total-")
|
||||
{
|
||||
# This is a bit sneaky. We spend every loop except the last
|
||||
# building up the -total- data, and then last time round,
|
||||
# we process it as another tbl, and push() the total values
|
||||
# into the image_data array.
|
||||
$data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
|
||||
}
|
||||
}
|
||||
push @tbl_data, \@col_data;
|
||||
}
|
||||
unshift @image_data, \@tbl_data;
|
||||
}
|
||||
|
||||
$vars->{tbl_field} = $field->{z};
|
||||
$vars->{col_field} = $field->{x};
|
||||
$vars->{row_field} = $field->{y};
|
||||
$vars->{time} = localtime(time());
|
||||
|
||||
$vars->{col_names} = \@col_names;
|
||||
$vars->{row_names} = \@row_names;
|
||||
$vars->{tbl_names} = \@tbl_names;
|
||||
|
||||
# Below a certain width, we don't see any bars, so there needs to be a minimum.
|
||||
if ($width && $ARGS->{format} eq "bar")
|
||||
{
|
||||
my $min_width = (scalar(@col_names) || 1) * 20;
|
||||
if (!$ARGS->{cumulate})
|
||||
{
|
||||
$min_width *= (scalar(@row_names) || 1);
|
||||
}
|
||||
$vars->{min_width} = $min_width;
|
||||
}
|
||||
|
||||
$vars->{width} = $width if $width;
|
||||
$vars->{height} = $height if $height;
|
||||
|
||||
$vars->{query} = $query;
|
||||
$vars = { %$vars, %{Bugzilla::Report->execute($ARGS)} };
|
||||
$vars->{saved_report_id} = $ARGS->{saved_report_id};
|
||||
$vars->{debug} = $ARGS->{debug};
|
||||
$vars->{report_columns} = Bugzilla::Search->REPORT_COLUMNS();
|
||||
|
||||
$ARGS->{ctype} = $ARGS->{action} eq 'plot' ? 'png' : ($ARGS->{format} eq 'csv' ? 'csv' : 'html');
|
||||
$ARGS->{format} = $ARGS->{format} eq 'csv' ? 'table' : $ARGS->{format};
|
||||
my $formatparam = $ARGS->{format};
|
||||
|
||||
if ($action eq "wrap")
|
||||
if ($action eq 'wrap')
|
||||
{
|
||||
# So which template are we using? If action is "wrap", we will be using
|
||||
# no format (it gets passed through to be the format of the actual data),
|
||||
|
@ -320,32 +127,22 @@ if ($action eq "wrap")
|
|||
$vars->{format} = $formatparam;
|
||||
$formatparam = '' if $formatparam ne 'simple';
|
||||
|
||||
# We need to keep track of the defined restrictions on each of the
|
||||
# axes, because buglistbase, below, throws them away. Without this, we
|
||||
# get buglistlinks wrong if there is a restriction on an axis field.
|
||||
$vars->{col_vals} = join("&", $buffer =~ /[&?]($field->{x}=[^&]+)/g);
|
||||
$vars->{row_vals} = join("&", $buffer =~ /[&?]($field->{y}=[^&]+)/g);
|
||||
$vars->{tbl_vals} = join("&", $buffer =~ /[&?]($field->{z}=[^&]+)/g);
|
||||
|
||||
# We need a number of different variants of the base URL for different URLs in the HTML.
|
||||
my $a = { %$ARGS };
|
||||
delete $a->{$_} for qw(x_axis_field y_axis_field z_axis_field ctype format query_format measure), @axis_fields;
|
||||
$vars->{buglistbase} = http_build_query($a);
|
||||
$a = { %$ARGS };
|
||||
delete $a->{$_} for $field->{z}, qw(action ctype format width height);
|
||||
delete $a->{$_} for $vars->{fields}->{z}, qw(action ctype format width height);
|
||||
$vars->{imagebase} = http_build_query($a);
|
||||
$a = { %$ARGS };
|
||||
delete $a->{$_} for qw(query_format action ctype format width height measure);
|
||||
for (keys %$a)
|
||||
{
|
||||
delete $a->{$_} if $a->{$_} eq '';
|
||||
}
|
||||
$vars->{switchparams} = $a;
|
||||
$vars->{switchbase} = http_build_query($a);
|
||||
$vars->{data} = \%data;
|
||||
}
|
||||
elsif ($action eq "plot")
|
||||
{
|
||||
# If action is "plot", we will be using a format as normal (pie, bar etc.)
|
||||
# and a ctype as normal (currently only png.)
|
||||
$vars->{cumulate} = $ARGS->{cumulate} ? 1 : 0;
|
||||
$vars->{x_labels_vertical} = $ARGS->{x_labels_vertical} ? 1 : 0;
|
||||
$vars->{data} = \@image_data;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -373,9 +170,9 @@ if ($ARGS->{debug})
|
|||
{
|
||||
require Data::Dumper;
|
||||
print "<pre>data hash:\n";
|
||||
print html_quote(Data::Dumper::Dumper(%data)) . "\n\n";
|
||||
print html_quote(Data::Dumper::Dumper($vars->{data})) . "\n\n";
|
||||
print "data array:\n";
|
||||
print html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>";
|
||||
print html_quote(Data::Dumper::Dumper($vars->{image_data})) . "\n\n</pre>";
|
||||
}
|
||||
|
||||
# All formats point to the same section of the documentation.
|
||||
|
@ -387,30 +184,3 @@ $template->process($format->{template}, $vars)
|
|||
|| ThrowTemplateError($template->error());
|
||||
|
||||
exit;
|
||||
|
||||
sub get_names
|
||||
{
|
||||
my ($names, $isnumeric, $field) = @_;
|
||||
|
||||
# These are all the fields we want to preserve the order of in reports.
|
||||
my $f = Bugzilla->get_field($field);
|
||||
if ($f && $f->is_select)
|
||||
{
|
||||
my $values = [ '', map { $_->name } @{ $f->legal_values(1) } ];
|
||||
my %dup;
|
||||
@$values = grep { exists($names->{$_}) && !($dup{$_}++) } @$values;
|
||||
return $values;
|
||||
}
|
||||
elsif ($isnumeric)
|
||||
{
|
||||
# It's not a field we are preserving the order of, so sort it
|
||||
# numerically...
|
||||
sub numerically { $a <=> $b }
|
||||
return [ sort numerically keys %$names ];
|
||||
}
|
||||
else
|
||||
{
|
||||
# ...or alphabetically, as appropriate.
|
||||
return [ sort keys %$names ];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,11 @@ our %FORMATS = map { $_ => 1 } qw(rss showteamwork);
|
|||
|
||||
my $who = $ARGS->{who};
|
||||
|
||||
my $fields = [ list($ARGS->{fields}) ];
|
||||
my $use_comments = (!$fields || grep { $_ eq 'longdesc' } @$fields) ? 1 : 0;
|
||||
my $use_fields = (!$fields || grep { $_ ne 'longdesc' } @$fields) ? 1 : 0;
|
||||
@$fields = grep { $_ ne 'longdesc' } @$fields;
|
||||
|
||||
my $limit;
|
||||
my $format = $ARGS->{ctype};
|
||||
trick_taint($format);
|
||||
|
@ -104,7 +109,7 @@ $join = "INNER JOIN $join i" if $join;
|
|||
# First query selects descriptions of new bugs and added comments (without duplicate information).
|
||||
# Worktime-only comments are excluded.
|
||||
# FIXME: Also use longdescs_history.
|
||||
my $longdescs = $dbh->selectall_arrayref(
|
||||
my $longdescs = $use_comments ? $dbh->selectall_arrayref(
|
||||
"SELECT
|
||||
b.bug_id, b.short_desc, pr.name product, cm.name component, bs.value, st.value,
|
||||
l.work_time, l.thetext,
|
||||
|
@ -125,10 +130,10 @@ my $longdescs = $dbh->selectall_arrayref(
|
|||
WHERE l.isprivate=0 ".($who ? " AND l.who=".$who->id : "")."
|
||||
AND l.bug_id$subq AND l.type!=".CMT_WORKTIME." AND l.type!=".CMT_BACKDATED_WORKTIME."
|
||||
ORDER BY l.bug_when DESC
|
||||
LIMIT $limit", {Slice=>{}});
|
||||
LIMIT $limit", {Slice=>{}}) : [];
|
||||
|
||||
# Second query selects bug field change history
|
||||
my $activity = $dbh->selectall_arrayref(
|
||||
my $activity = $use_fields ? $dbh->selectall_arrayref(
|
||||
"SELECT
|
||||
b.bug_id, b.short_desc, pr.name product, cm.name component, bs.value, st.value,
|
||||
0 AS work_time, '' thetext,
|
||||
|
@ -149,8 +154,9 @@ my $activity = $dbh->selectall_arrayref(
|
|||
LEFT JOIN fielddefs f ON f.id=a.fieldid
|
||||
LEFT JOIN attachments at ON at.attach_id=a.attach_id
|
||||
WHERE at.isprivate=0 ".($who ? " AND a.who=".$who->id : "")." AND a.bug_id$subq
|
||||
".(@$fields ? " AND f.name IN (".join(", ", ("?") x @$fields).")" : "")."
|
||||
ORDER BY a.bug_when DESC, f.name ASC
|
||||
LIMIT $limit", {Slice=>{}});
|
||||
LIMIT $limit", {Slice=>{}}, @$fields) : [];
|
||||
|
||||
my $events = [ sort {
|
||||
($b->{bug_when} cmp $a->{bug_when}) ||
|
||||
|
|
|
@ -156,6 +156,8 @@ $vars->{comment_indexes} = sub
|
|||
return [ map { [ $_->{count}, $_->{comment_id}, $_->{type} != CMT_WORKTIME && $_->{type} != CMT_BACKDATED_WORKTIME ? 1 : 0 ] } @$comments ];
|
||||
};
|
||||
|
||||
$vars->{look_in_urls} = [ map { [ split /:/, $_, 2 ] } grep { /^(?!\s*(#|$))/so } split /\n/, Bugzilla->params->{look_in_urls} ];
|
||||
|
||||
Bugzilla->cgi->send_header($format->{ctype});
|
||||
$template->process($format->{template}, $vars)
|
||||
|| ThrowTemplateError($template->error());
|
||||
|
|
|
@ -946,13 +946,16 @@ table.tabs {
|
|||
}
|
||||
|
||||
table.eventbox {
|
||||
border-color: gray;
|
||||
padding: 5px;
|
||||
border-color: white;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
table.eventbox td.set {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
/* search */
|
||||
|
||||
#summary_field.search_field_row input {
|
||||
|
|
|
@ -95,16 +95,16 @@ td.forbidden {
|
|||
}
|
||||
|
||||
table.eventbox {
|
||||
border: 1px solid black;
|
||||
border: 10px solid white;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
td.set {
|
||||
table.eventbox td.set {
|
||||
background: #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
td.unset {
|
||||
table.eventbox td.unset {
|
||||
color: red;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
th.sorttable_sorted a:after { content: "▲"; color: gray; }
|
||||
th.sorttable_sorted_reverse a:after { content: "▼"; color: gray; }
|
||||
th.sorted_0.sorttable_sorted a:after { content: "▲"; color: black; }
|
||||
th.sorted_0.sorttable_sorted_reverse a:after { content: "▼"; color: black; }
|
||||
th.sorttable_sorted a:after { content: "\25B2";/*▲*/ color: gray; }
|
||||
th.sorttable_sorted_reverse a:after { content: "\25BC";/*▼*/ color: gray; }
|
||||
th.sorted_0.sorttable_sorted a:after { content: "\25B2";/*▲*/ color: black; }
|
||||
th.sorted_0.sorttable_sorted_reverse a:after { content: "\25BC";/*▼*/ color: black; }
|
||||
th.sorted_0 { background-color: #a0a0a0; }
|
||||
th.sorted_1 { background-color: #acacac; }
|
||||
th.sorted_2 { background-color: #b8b8b8; }
|
||||
|
@ -78,7 +78,8 @@ td.bz_total {
|
|||
.bz_buglist .bz_estimated_time_column,
|
||||
.bz_buglist .bz_remaining_time_column,
|
||||
.bz_buglist .bz_work_time_column,
|
||||
.bz_buglist .bz_percentage_complete_column { text-align: right; }
|
||||
.bz_buglist .bz_percentage_complete_column,
|
||||
.bz_buglist .bz_f30 { text-align: right; }
|
||||
|
||||
.bz_buglist .bz_short_desc_column,
|
||||
.bz_buglist .bz_short_short_desc_column,
|
||||
|
@ -87,7 +88,8 @@ td.bz_total {
|
|||
.bz_buglist .bz_flagtypes_name_column,
|
||||
.bz_buglist .bz_f1,
|
||||
.bz_buglist .bz_f4,
|
||||
.bz_buglist .bz_f7 { white-space: normal; }
|
||||
.bz_buglist .bz_f7,
|
||||
.bz_buglist .bz_customfield { white-space: normal; }
|
||||
|
||||
.bz_buglist .bz_comment0_column,
|
||||
.bz_buglist .bz_lastcomment_column { width: 15%; white-space: normal; }
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
.bz_comment .attachment_image { max-width: 50em; margin: 10px 0px 0px 0px; }
|
||||
|
||||
.bz_comment_text.bz_fullscreen_comment { min-width: 50em; width: 100%; word-wrap: break-word; }
|
||||
#commentpreviewhtml .bz_comment_text.bz_fullscreen_comment { min-width: 0px; }
|
||||
|
||||
.bz_comment .bz_fmt_table td, .bz_comment .bz_fmt_table th { border: 1px solid gray; padding: 5px; }
|
||||
|
||||
#comments > table, .bz_section_additional_comments > table { table-layout: fixed; }
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 116.86519 32.210506"
|
||||
height="32.210506mm"
|
||||
preserveAspectRatio="none"
|
||||
width="116.86519mm">
|
||||
<g
|
||||
transform="translate(483.18298,-59.147989)"
|
||||
id="layer1">
|
||||
<path
|
||||
id="path819"
|
||||
d="M -483.05357,59.630952 -366.4472,90.875534"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 782 B |
|
@ -59,3 +59,11 @@ dd {
|
|||
padding: .5em;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
pre.cfg_example {
|
||||
margin: 8px 0;
|
||||
padding: 4px;
|
||||
background: white;
|
||||
float: left;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
|
|
@ -61,3 +61,7 @@
|
|||
vertical-align: middle;
|
||||
min-width: 8em;
|
||||
}
|
||||
|
||||
table.report td {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
|
|
@ -92,16 +92,11 @@ sub sqlize_dates
|
|||
{
|
||||
# we've checked, trick_taint is fine
|
||||
trick_taint($start_date);
|
||||
$date_bits = " AND longdescs.bug_when > ?";
|
||||
$date_bits = " AND longdescs.bug_when >= ?";
|
||||
push @date_values, $start_date;
|
||||
}
|
||||
if ($end_date)
|
||||
{
|
||||
# we need to add one day to end_date to catch stuff done today
|
||||
# do not forget to adjust date if it was the last day of month
|
||||
my (undef, undef, undef, $ed, $em, $ey, undef) = strptime($end_date);
|
||||
($ey, $em, $ed) = date_adjust($ey+1900, $em+1, $ed, 1);
|
||||
$end_date = sprintf("%04d-%02d-%02d", $ey, $em, $ed);
|
||||
$date_bits .= " AND longdescs.bug_when < ?";
|
||||
push @date_values, $end_date;
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ my $i = 0;
|
|||
for my $q (@$queries)
|
||||
{
|
||||
my $key = $q->{userid}.':'.$q->{name};
|
||||
my $user = Bugzilla->request_cache->{user} = Bugzilla::User->new({ id => $q->{userid} });
|
||||
my $user = Bugzilla::User->new({ id => $q->{userid} });
|
||||
print("Invalid user $q->{userid}!\n"), next unless $user;
|
||||
next if $user->disabledtext;
|
||||
my $s = "Testing $q->{userid}'s $q->{name}... ";
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
<label for="Bugzilla_restrictlogin" title="Restrict this session to this IP address (using this option improves security)">Restrict to IP</label>
|
||||
|
||||
[% PROCESS "global/hidden-fields.html.tmpl"
|
||||
exclude="^Bugzilla_(login|password|restrictlogin)$" %]
|
||||
exclude="^Bugzilla_(login|password|restrictlogin)$|^logout$|^GoAheadAndLogIn$" %]
|
||||
|
||||
<input type="submit" name="GoAheadAndLogIn" value="Log in to [% terms.Bugzilla %]" id="log_in" />
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
[%# Use the current script name. If an empty name is returned,
|
||||
# then we are accessing the home page. %]
|
||||
|
||||
[% cgi.delete('logout') %]
|
||||
[% login_target = cgi.url("-relative" => 1, "-query" => 1) %]
|
||||
[% IF !login_target OR login_target.match("^token.cgi") %]
|
||||
[% login_target = "index.cgi" %]
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
</table>
|
||||
|
||||
[% PROCESS "global/hidden-fields.html.tmpl"
|
||||
exclude="^Bugzilla_(login|password|restrictlogin)$" %]
|
||||
exclude="^Bugzilla_(login|password|restrictlogin)$|^logout$|^GoAheadAndLogIn$" %]
|
||||
|
||||
<input type="submit" name="GoAheadAndLogIn" value="Log in" id="log_in" />
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
<option value="[% q.id %]" [% " selected='selected'" IF checker.query_id == q.id %] >[% q.name | html %]</option>
|
||||
[% END %]
|
||||
[% IF checker.query_id AND !found %]
|
||||
<option value="[% checker.query_id %]" selected='selected'>(Чужой) [% checker.query.name | html %]</option>
|
||||
<option value="[% checker.query_id %]" selected='selected'>([% checker.query.user.login | html %]) [% checker.query.name | html %]</option>
|
||||
[% END %]
|
||||
</select></td>
|
||||
</tr>
|
||||
|
@ -128,6 +128,18 @@
|
|||
<label for="is_fatal">Жёсткий запрет (если нет, то изменения не блокируются, а только даётся предупреждение)</label><br />
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th style="text-align: left" colspan="2">Разрешить следующим пользователям обходить эту проверку:</th></tr>
|
||||
<tr>
|
||||
<th>Группа:</th>
|
||||
<td>
|
||||
<select name="bypass_group_id" id="bypass_group_id">
|
||||
<option value="">— (не разрешать)</option>
|
||||
[% FOR g = all_groups %]
|
||||
<option value="[% g.id %]" [% " selected=\"selected\"" IF checker.bypass_group_id == g.id %]>[% g.name | html %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><th style="text-align: left" colspan="2">Запреты изменений отдельных полей, действуют только при обновлении багов:</th></tr>
|
||||
<tr>
|
||||
<th style="background: #FFE0E0">Запрещать:</th>
|
||||
|
@ -222,7 +234,12 @@ function check_trigger()
|
|||
showhide_allowdeny();
|
||||
[% IF checker.except_fields %]
|
||||
[% FOR f = checker.except_fields.keys %]
|
||||
add_field("[% f | js %]", "[% checker.except_fields.$f | js %]");
|
||||
[% IF !checker.except_fields.$f %]
|
||||
add_field("[% f | js %]", "");
|
||||
[% END %]
|
||||
[% FOR v = checker.except_fields.$f %]
|
||||
add_field("[% f | js %]", "[% v | js %]");
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% ELSE %]
|
||||
add_field();
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
<option value="[% v.id %]"[% ' selected="selected"' IF cur_default.${v.id} %]>[% v.name | html %]</option>
|
||||
[% END %]
|
||||
</select>
|
||||
[% ELSIF f.type == constants.FIELD_TYPE_TEXTAREA %]
|
||||
[% ELSIF f.type == constants.FIELD_TYPE_TEXTAREA || f.type == constants.FIELD_TYPE_EAV_TEXTAREA %]
|
||||
<textarea cols="50" rows="5" name="default_[% f.name | html %]" id="default_[% f.name | html %]">[% f.get_default_value(this_value.id) | html %]</textarea>
|
||||
[% ELSE %]
|
||||
<input type="text" name="default_[% f.name | html %]" id="default_[% f.name | html %]" value="[% f.get_default_value(this_value.id) | html %]" />
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# Authors: Vitaliy Filippov <vitalif@mail.ru>, Vladimir Koptev <vladimir.koptev@gmail.com>
|
||||
#%]
|
||||
|
||||
[% SET title = "Select Active " _ field.description _ " Objects For " _ field.value_field.description _ ' ' _ visibility_value.name | html %]
|
||||
[% SET title = "Select Active " _ field.description _ " Objects For " _ field.value_field.description _ ' ' _ visibility_value.full_name | html %]
|
||||
|
||||
[% PROCESS global/header.html.tmpl %]
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
[% FOREACH field_value = field.value_field.legal_values %]
|
||||
[% IF field.visibility_field_id != field.value_field_id || field.has_visibility_value(field_value.id) %]
|
||||
<option value="[% field_value.id | none %]" [% ' selected="selected"' IF field.is_value_enabled(value.id, field_value.id) %]>
|
||||
[%- field_value.name | html -%]
|
||||
[%- field_value.full_name | html -%]
|
||||
</option>
|
||||
[% END %]
|
||||
[% END %]
|
||||
|
|
|
@ -31,32 +31,37 @@
|
|||
"If this is on, $terms.Bugzilla will by default associate newly created groups"
|
||||
_ " with each product in the database. Generally only useful for small databases.",
|
||||
|
||||
chartgroup => "The name of the group of users who can use the 'New Charts' " _
|
||||
"feature. Administrators should ensure that the public categories " _
|
||||
"and series definitions do not divulge confidential information " _
|
||||
"before enabling this for an untrusted population. If left blank, " _
|
||||
"no users will be able to use New Charts.",
|
||||
chartgroup =>
|
||||
"The name of the group of users who can use the 'New Charts' "
|
||||
_ "feature. Administrators should ensure that the public categories "
|
||||
_ "and series definitions do not divulge confidential information "
|
||||
_ "before enabling this for an untrusted population. If left blank, "
|
||||
_ "no users will be able to use New Charts.",
|
||||
|
||||
insidergroup => "The name of the group of users who can see/change private " _
|
||||
"comments and attachments.",
|
||||
insidergroup =>
|
||||
"The name of the group of users who can see/change private "
|
||||
_ "comments and attachments.",
|
||||
|
||||
timetrackinggroup => "The name of the group of users who can see/change time tracking " _
|
||||
"information.",
|
||||
timetrackinggroup =>
|
||||
"The name of the group of users who can see/change time tracking information.",
|
||||
|
||||
querysharegroup => "The name of the group of users who can share their " _
|
||||
"saved searches with others.",
|
||||
querysharegroup =>
|
||||
"The name of the group of users who can share their saved searches with others.",
|
||||
|
||||
usevisibilitygroups =>
|
||||
"<p>Do you wish to restrict visibility of users to members of specific groups,"
|
||||
_ " based on the configuration specified in group settings?</p>"
|
||||
_ "<p>If yes, each group can be allowed to see members of selected other groups.</p>",
|
||||
|
||||
strict_isolation => "Don't allow users to be assigned to, " _
|
||||
"be qa-contacts on, " _
|
||||
"be added to CC list, " _
|
||||
"or make or remove dependencies " _
|
||||
"involving any bug that is in a product on which that " _
|
||||
"user is forbidden to edit.",
|
||||
strict_isolation =>
|
||||
"Don't allow users to be assigned to, be qa-contacts on, "
|
||||
_ "be added to CC list, or make or remove dependencies "
|
||||
_ "involving any bug that is in a product on which that "
|
||||
_ "user is forbidden to edit.",
|
||||
|
||||
forbid_open_products =>
|
||||
"Don't allow 'open' products, i.e. force everyone to set at least"
|
||||
_ " one MANDATORY/MANDATORY and one ENTRY group for each product."
|
||||
_ " This is checked for new products and for products whose group controls are being modified.",
|
||||
}
|
||||
%]
|
||||
|
|
|
@ -18,18 +18,15 @@
|
|||
_ " \$EMAIL will be replaced by cleartext user email, so you should only never use it in public networks;"
|
||||
_ " \$MD5 will be replaced by MD5 hash of user email, just like it is required by real Gravatar service."
|
||||
_ " You can also disable avatar display by clearing this parameter.",
|
||||
viewvc_url =>
|
||||
"ViewVC query URL for browsing bug-related commits. Bugzilla4Intranet links to ViewVC search page with full-text query 'bugXXX XXX'"
|
||||
_ " which allows to show commits with the bug number in the commit message (like 'Bug XXX' or 'BugXXX').",
|
||||
git_url =>
|
||||
"Git (GitBlit or other web UI) full-text query URL for browsing bug-related commits. '$q' in URL will be replaced by 'bugXXX XXX'"
|
||||
_ " which allows to show commits with the bug number in the commit message (like 'Bug XXX' or 'BugXXX').",
|
||||
look_in_urls =>
|
||||
"<p style='margin: 0'>VCS/Wiki/whatever query URLs for 'Look for bug in ...' links, one per line, separated by ':'.</p>" _
|
||||
"<pre class='cfg_example'>CVS/SVN: http://viewvc.local/?view=query&comment=bug\$BUG+\$BUG&comment_match=fulltext&querysort=date&date=all</pre>" _
|
||||
"<p style='margin: 0; clear: both'>$BUG will be replaced with bug ID in these URLs.</p>",
|
||||
wiki_url =>
|
||||
"Default MediaWiki URL for bug links. Bugzilla4Intranet links to <tt><wiki_url>/Bug_XXX</tt> pages when this is non-empty.",
|
||||
mediawiki_urls =>
|
||||
"<p style='margin: 0'>Known MediaWiki URLs to be quoted in bug comments, one per line. Example:</p>"
|
||||
_ "<pre style='margin: 8px 0; padding: 4px; background: white; float: left;"
|
||||
_ " border: 1px solid gray;'>wikipedia http://en.wikipedia.org/wiki/</pre>"
|
||||
_ "<pre class='cfg_example'>wikipedia http://en.wikipedia.org/wiki/</pre>"
|
||||
_ "<p style='margin: 0; clear: both'>Links like <b><tt>wikipedia:Article_name#Section</tt></b>"
|
||||
_ " and <b><tt>wikipedia:[[Article name#Section]]</tt></b><br /> will be quoted"
|
||||
_ " and lead to <b>Section</b> (optional) of <b>Article name</b> page in the Wikipedia.</p>",
|
||||
|
|
|
@ -56,6 +56,14 @@
|
|||
_ " won't get sent). This affects all mail sent by $terms.Bugzilla,"
|
||||
_ " not just $terms.bug updates.",
|
||||
|
||||
enable_inmail_cgi =>
|
||||
"Enable HTTP handler for incoming e-mail (email_in.cgi). " _
|
||||
"<b>IMPORTANT NOTE:</b> This handler is only for usage from your MTA. " _
|
||||
"If you enable it, you MUST make sure that your nginx (or other http " _
|
||||
"reverse proxy Bugzilla4Intranet is installed behind) denies access to " _
|
||||
"email_in.cgi from public addresses. " _
|
||||
"See the end of email_in.cgi for an example Postfix configuration.",
|
||||
|
||||
sendmailnow => "Sites using anything older than version 8.12 of 'sendmail' " _
|
||||
"can achieve a significant performance increase in the " _
|
||||
"UI -- at the cost of delaying the sending of mail -- by " _
|
||||
|
|
|
@ -63,4 +63,8 @@
|
|||
stem_language => "Language for stemming words in full-text search, 2-letter code" _
|
||||
" (one of: da, de, en, es, fi, fr, hu, it, nl, no, pt, ro, ru, sv, tr)",
|
||||
|
||||
sphinx_max_matches =>
|
||||
"Set it to the same value as max_matches in your Sphinx search configuration. " _
|
||||
"Default is 1000 and if it's not enough you may sometimes miss some search results when using Sphinx.",
|
||||
|
||||
} %]
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue