Compare commits

...

61 Commits

Author SHA1 Message Date
Vitaliy Filippov 655d27d543 Fix link quote regexp which could infinite loop in rare cases
Namely https://elk-dev.modeus.me/app/kibana?#/discover?_g=(filters:!(),refreshInterval:(display:Off,pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))&_a=(columns:!(level,service_name,message,docker_service,docker_container),filters:!(('$$hashKey':'object:10530','$state':(store:appState),meta:(alias:!n,disabled:!t,index:'logstash-*',key:service_name,negate:!f,value:periodplanning),query:(match:(service_name:(query:periodplanning,type:phrase)))),('$$hashKey':'object:10788','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:docker_container,negate:!f,value:dev-2),query:(match:(docker_container:(query:dev-2,type:phrase)))),('$$hashKey':'object:19295','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:EventId.Name,negate:!t,value:Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted),query:(match:(EventId.Name:(query:Microsoft.EntityFrameworkCore.Database.Command.CommandExecuted,type:phrase)))),('$$hashKey':'object:20835','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:SourceContext,negate:!t,value:CUSTIS.Modeus.PeriodPlanning.Api.MinioProvider),query:(match:(SourceContext:(query:CUSTIS.Modeus.PeriodPlanning.Api.MinioProvider,type:phrase)))),('$$hashKey':'object:21337','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:EventId.Name,negate:!t,value:Microsoft.EntityFrameworkCore.Query.QueryClientEvaluationWarning),query:(match:(EventId.Name:(query:Microsoft.EntityFrameworkCore.Query.QueryClientEvaluationWarning,type:phrase)))),('$$hashKey':'object:22315','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:EventId.Name,negate:!t,value:Microsoft.EntityFrameworkCore.Query.IncludeIgnoredWarning),query:(match:(EventId.Name:(query:Microsoft.EntityFrameworkCore.Query.IncludeIgnoredWarning,type:phrase)))),('$$hashKey':'object:22807','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:EventId.Name,negate:!t,value:Microsoft.EntityFrameworkCore.Query.FirstWithoutOrderByAndFilterWarning),query:(match:(EventId.Name:(query:Microsoft.EntityFrameworkCore.Query.FirstWithoutOrderByAndFilterWarning,type:phrase)))),('$$hashKey':'object:23246','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:SourceContext,negate:!t,value:CUSTIS.Modeus.PeriodPlanning.Services.ConsolidatedPlanService),query:(match:(SourceContext:(query:CUSTIS.Modeus.PeriodPlanning.Services.ConsolidatedPlanService,type:phrase)))),('$$hashKey':'object:23754','$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:SourceContext,negate:!t,value:CUSTIS.Modeus.PeriodPlanning.Services.ContingentService),query:(match:(SourceContext:(query:CUSTIS.Modeus.PeriodPlanning.Services.ContingentService,type:phrase))))),index:'logstash-*',interval:auto,query:(query_string:(analyze_wildcard:!t,query:'service_name:%20"periodplanning"')),sort:!('@timestamp',desc))

:-P
2019-12-25 20:35:12 +03:00
Vitaliy Filippov ce04ad9344 Remove objects from logged error messages 2019-12-25 19:58:40 +03:00
Vitaliy Filippov bbcb30140c Add regex_escape() TT function 2019-08-21 15:36:47 +03:00
Vitaliy Filippov ae264bdf2d Fix Bug.update WS comment parameter handling 2019-08-09 15:55:57 +03:00
Vitaliy Filippov 5e83139625 Add bugLinkHook to allow customisation of clone links 2019-06-05 15:59:43 +03:00
Vitaliy Filippov cb0f6d025c raise_error=0 works strange 2019-05-23 18:34:47 +03:00
Vitaliy Filippov 02e716777f Clear external product references when deleting product 2018-12-21 18:37:38 +03:00
Vitaliy Filippov d3257ad482 Check if the product is "open" explicitly in editproducts.cgi until we have a single "save" method 2018-12-21 18:27:54 +03:00
Vitaliy Filippov d31c230159 Add "forbid_open_products" setting 2018-12-21 18:12:41 +03:00
Vitaliy Filippov 7c13c5e36e Previous fix breaks not-equals-multiselects 2018-10-04 16:36:35 +03:00
Vitaliy Filippov 935805170e Also fix "is not equal to XXX" not matching NULL values 2018-10-01 17:11:38 +03:00
Vitaliy Filippov 4212177a2d Fix "xxx does not contains yyy AND (...)" not matching NULL values 2018-10-01 16:53:17 +03:00
Vitaliy Filippov 06fc8350ac Fix refreshing views with bad saved queries 2018-08-30 16:13:59 +03:00
Vitaliy Filippov 05cd2beaf8 Allow to filter rss-comments.cgi by fields 2018-07-11 13:09:28 +03:00
Vitaliy Filippov d6f3c1c869 Fix moving worktime 2018-07-06 17:58:49 +03:00
Vitaliy Filippov 64341d027c Allow balanced round braces in URLs 2018-06-01 15:53:52 +03:00
Vitaliy Filippov 08c91c0007 Oops 2018-05-30 19:36:37 +03:00
Vitaliy Filippov 374efe3279 Fix CSV format 2018-05-30 19:20:38 +03:00
Vitaliy Filippov 0191669861 Support numeric fields in reports 2018-05-30 16:26:36 +03:00
Vitaliy Filippov bf83ef7a98 Make report-table styles prettier 2018-05-30 16:25:53 +03:00
Vitaliy Filippov 84dfe3918c Remove useless "data" files from git 2018-05-29 20:08:22 +03:00
Vitaliy Filippov 4b7a51efa8 Calculate totals over numeric columns 2018-05-29 19:55:06 +03:00
Vitaliy Filippov a4f77c4340 Fix etime/wtime/rtime reports 2018-05-23 15:01:56 +03:00
Vitaliy Filippov 3f66ba7490 Allow any number of last days in TodayWorktime 2018-05-23 14:45:28 +03:00
Vitaliy Filippov 7683087937 Adjust remaining time in "fix worktime" 2018-05-23 14:38:33 +03:00
Vitaliy Filippov f9545db73b Do not adjust remaining time on form submit 2018-05-23 14:10:06 +03:00
Vitaliy Filippov d538552033 Do not zero remaining_time on close 2018-05-21 15:08:53 +03:00
Vitaliy Filippov 0c553e6a0e Remove min-width in comment preview 2018-05-21 14:36:16 +03:00
Vitaliy Filippov d34758d028 Format tables as HTML, not as ASCII pseudographic 2018-05-21 14:27:14 +03:00
Vitaliy Filippov 3e36282959 Allow object in set_product 2018-05-18 16:04:03 +03:00
Vitaliy Filippov 861ce067d9 Better display of textareas in bug list 2018-05-18 15:51:27 +03:00
Vitaliy Filippov 94d624b036 Fix crash when trying to view report.cgi with an inaccessible column 2018-04-18 14:55:22 +03:00
Vitaliy Filippov fe7734fbc6 Add "External bug Deadline" 2018-04-06 17:00:11 +03:00
Vitaliy Filippov 4ac55c1121 Support commentsilent by using send_mail instead of BugMail::send 2018-04-03 14:46:57 +03:00
Vitaliy Filippov a1e873904f "Large Text Box (separate table)" field type 2018-03-28 14:44:30 +03:00
Vitaliy Filippov 65e825d471 Bug 232765 - Refresh field cache after editing product permissions or user groups 2018-03-22 14:18:54 +03:00
Vitaliy Filippov 3b9ba2b7b5 Remove send_changes (Bugzilla 5.0), use our Bugzilla->send_mail 2018-03-21 16:46:32 +03:00
Vitaliy Filippov 5f396a2d77 Fix "wide character in subroutine entry" 2017-12-29 14:44:58 +03:00
Vitaliy Filippov e395f89f73 HTTP incoming email handler 2017-12-28 00:55:23 +03:00
Vitaliy Filippov 0122af7a78 Fix mass import 2017-11-30 19:54:36 +03:00
Vitaliy Filippov c8b8ccd383 Default /usr/bin/dot 2017-11-30 18:12:39 +03:00
Vitaliy Filippov fcc7be34af Bug 222834 - Fix warning 2017-10-05 12:39:08 +03:00
Vitaliy Filippov 669b67215b Bug 222834 - Warn when trying to post worktime without comment 2017-10-04 18:26:40 +03:00
Vitaliy Filippov 155e56dfcb Fix xmlrpc with read/sysread clutter 2017-09-26 19:08:43 +03:00
Vitaliy Filippov 257d4b0ebb Hide BUG_ID_REV from edit-multiple 2017-09-22 13:39:32 +03:00
Vitaliy Filippov d63bc27928 Fix split by month in summarize_time 2017-09-08 17:52:21 +03:00
Vitaliy Filippov 349af79c2e Add type into Bug.add_comment WS 2017-04-24 15:24:42 +03:00
Vitaliy Filippov 917f3e1566 Fix WebService.Bug.add_comment 2017-04-24 15:17:51 +03:00
Vitaliy Filippov e08c5f3a6b Fix bug apis (missing backported method) 2017-04-24 14:26:11 +03:00
Vitaliy Filippov 9f6262a6e3 Fix report-simple for multiple tables 2017-04-14 13:54:04 +03:00
Vitaliy Filippov f7321af1b1 Fix mouseout 2017-04-04 14:58:37 +03:00
Vitaliy Filippov 4ae73720a8 Throw for invalid fulltext queries 2017-02-15 15:22:47 +03:00
Vitaliy Filippov a8c63de19d Pass smtp_debug parameter to SMTP transport, remove sysread hack (breaks Email::Sender::Transport::SMTP) 2017-02-10 17:35:04 +03:00
Vitaliy Filippov 97aa1f1787 Prevent invalid utf8 in decoded wiki links, prevent infinite loops in html regex 2017-02-07 15:10:46 +03:00
Vitaliy Filippov 7802af4bc9 Disable server push for IE11/Edge 2017-02-01 16:58:12 +03:00
Vitaliy Filippov 3a9b5f93f5 Allow to create administration groups for created products 2017-01-25 17:29:38 +03:00
Vitaliy Filippov a7609559c5 Config for newer sphinx 2017-01-10 17:46:06 +03:00
Vitaliy Filippov ba08f1f46d Allow to raise max_matches params for Sphinx search 2017-01-10 15:20:07 +03:00
Vitaliy Filippov a608641a2b Always add at least one bug to INSERT statement 2016-12-22 22:00:32 +03:00
Vitaliy Filippov ca2014f6ce Show joined email fields in search 2016-12-15 13:46:03 +03:00
Vitaliy Filippov 6a9039b06e Fix editvisibility based on component 2016-09-15 17:56:09 +03:00
69 changed files with 1428 additions and 1402 deletions

View File

@ -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,8 +495,7 @@ 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;
@ -564,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);
@ -607,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};
@ -751,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};
@ -916,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);
@ -929,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 });
@ -1080,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
@ -1441,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) = @_;
@ -1488,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} || []})
{
@ -1504,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
@ -1677,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.
@ -2156,18 +2203,32 @@ sub _set_keywords
sub _set_product
{
my ($self, $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)
my $product;
if (!ref $name)
{
return undef;
$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)
{
return undef;
}
# Check that the product exists and that the user
# is allowed to enter bugs into this 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.
$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);
}
# Check that the product exists and that the user
# is allowed to enter bugs into this 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 });
if (($self->product_id || 0) != $product->id)
{
$self->{product_id} = $product->id;
@ -3635,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)
@ -4295,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};
}
@ -4321,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 });
@ -4346,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 ] })),
};

View File

@ -91,26 +91,32 @@ sub new
my $product;
if (ref $param)
{
$product = $param->{product};
my $name = $param->{name};
if (!defined $product)
if ($param->{id})
{
ThrowCodeError('bad_arg', {
argument => 'product',
function => "${class}::new",
});
$param = { condition => 'id = ?', values => [ $param->{id} ] };
}
if (!defined $name)
else
{
ThrowCodeError('bad_arg', {
argument => 'name',
function => "${class}::new",
});
$product = $param->{product};
my $name = $param->{name};
if (!defined $product)
{
ThrowCodeError('bad_arg', {
argument => 'product',
function => "${class}::new",
});
}
if (!defined $name)
{
ThrowCodeError('bad_arg', {
argument => 'name',
function => "${class}::new",
});
}
my $condition = 'product_id = ? AND name = ?';
my @values = ($product->id, $name);
$param = { condition => $condition, values => \@values };
}
my $condition = 'product_id = ? AND name = ?';
my @values = ($product->id, $name);
$param = { condition => $condition, values => \@values };
}
unshift @_, $param;
@ -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 ####

View File

@ -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
},

View File

@ -109,6 +109,12 @@ sub get_param_list
type => 'b',
default => 0
},
{
name => 'forbid_open_products',
type => 'b',
default => 0
},
);
return @param_list;
}

View File

@ -103,6 +103,12 @@ sub get_param_list
default => 1
},
{
name => 'enable_inmail_cgi',
type => 'b',
default => 0
},
{
name => 'smtpserver',
type => 't',

View File

@ -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;
}

View File

@ -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
@ -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;

View File

@ -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,14 +838,27 @@ 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);
}
$self->bz_drop_table($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

View File

@ -1326,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'},
],
};
#--------------------------------------------------------------------------

View File

@ -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} };
@ -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;

View File

@ -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 });
}
@ -893,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);
}
# 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);
@ -1278,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);
}
# 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)
{

View File

@ -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

View File

@ -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();

462
Bugzilla/InMail.pm Normal file
View File

@ -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__

View File

@ -3523,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;
}

View File

@ -130,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',

View File

@ -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',

View File

@ -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',

View File

@ -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,7 +649,7 @@ use constant is_default => 0;
sub _create_bug_group
{
my $self = shift;
my ($create_admin_group) = @_;
my ($create_admin_group, $normal_group) = @_;
my $dbh = Bugzilla->dbh;
my $group_name = ($create_admin_group ? 'admin-' : '') . $self->name;
@ -631,9 +671,9 @@ sub _create_bug_group
# Associate the new group and new product.
$dbh->do(
'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol, editcomponents)'.
' VALUES (?, ?, ?, ?, ?)', undef, $group->id, $self->id,
($create_admin_group ? (0, 0, 1) : (CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0))
'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
@ -641,6 +681,17 @@ sub _create_bug_group
'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

View File

@ -131,6 +131,23 @@ sub _get_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;
@ -206,29 +223,45 @@ sub execute
}
}
my $measures = {
my $measure_alias = {
etime => 'estimated_time',
rtime => 'remaining_time',
wtime => 'interval_time',
count => '_count',
};
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
my %old_columns = %{Bugzilla::Search->COLUMNS($runner)};
%{Bugzilla::Search->COLUMNS($runner)} = (%{Bugzilla::Search->COLUMNS($runner)}, %{Bugzilla::Search->REPORT_COLUMNS});
Bugzilla::Search->COLUMNS($runner)->{_count}->{name} = '1';
%{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(etime rtime wtime) : $measure)
for ($measure eq 'times' ? qw(estimated_time remaining_time interval_time) : $measure)
{
push @axis_fields, $measures->{$_} unless $a{$measures->{$_}};
}
@ -245,7 +278,7 @@ sub execute
($field->{x} || "''")." x, ".
($field->{y} || "''")." y, ".
($field->{z} || "''")." z, ".
join(', ', map { "SUM($measures->{$_}) $_" } $measure eq 'times' ? qw(etime rtime wtime) : $measure).
join(', ', map { "SUM($measures->{$_}) $_" } ($measure eq 'times' ? qw(etime rtime wtime) : $measure)).
" FROM ($query) _report_table GROUP BY ".join(", ", @group_by);
$::SIG{TERM} = 'DEFAULT';
@ -258,11 +291,9 @@ sub execute
my %data;
my %names;
# Read the bug data and count the bugs for each possible value of row, column
# and table.
# 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.
# We detect a numerical field, and sort appropriately, if all the values are numeric.
my %isnumeric;
foreach my $group (@$results)
@ -363,7 +394,7 @@ sub execute
$vars->{cumulate} = $ARGS->{cumulate} ? 1 : 0;
$vars->{x_labels_vertical} = $ARGS->{x_labels_vertical} ? 1 : 0;
%{Bugzilla::Search->COLUMNS($runner)} = %old_columns;
%{Bugzilla::Search->COLUMNS($runner)} = %$old_columns;
return $vars;
}

View File

@ -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
@ -614,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;
}
}
}
@ -646,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} = {
@ -661,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;
@ -688,6 +723,17 @@ sub STATIC_COLUMNS
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,
};
}
}
}
@ -746,35 +792,51 @@ sub COLUMNS
# 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 (!$user->id)
{
$login = $dbh->sql_string_until($login, $dbh->quote('@'));
}
$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)";
$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 = ($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)";
$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(($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} = $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 });
@ -953,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,
@ -2536,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})
@ -2544,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),
@ -2612,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});
@ -3049,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)";
}
}
@ -3372,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)

View File

@ -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;
}
$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 = { '<' => '&lt;', '>' => '&gt;', '&' => '&amp;', '"' => '&quot;' };
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} : ($1 eq '' ? lc($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);
@ -425,13 +425,12 @@ sub unquote_wiki_url
my $a;
$anchor = $1;
# decode MediaWiki page section name (only correct UTF8 sequences)
$anchor =~ s/(
$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!.!\%!), ($a)/gesx;
$anchor = url_decode($anchor);
)+)/($a = $1), ($a =~ tr!.!\%!), (url_decode($a))/gesx;
$anchor =~ tr/_/ /;
}
$article =~ s/&.*$//so if $wikiurl =~ /title=$/so;
@ -440,8 +439,11 @@ sub unquote_wiki_url
Encode::_utf8_on($linkurl);
Encode::_utf8_on($article);
Encode::_utf8_on($anchor);
$linkurl = '<a href="'.html_quote($wikiurl.$linkurl).'">'.$wikiname.':[['.$article.($anchor eq '' ? '' : '#'.$anchor).']]</a>';
return $linkurl;
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>";
@ -954,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:

View File

@ -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 || ' '.('&nbsp;' x (length($2)-1))/ge;
if (!$p && $m[0] ne '')
if (!$p && $m[0] ne '' && !$m[2])
{
$text .= '<p>';
$p = 1;
@ -625,15 +625,15 @@ sub bz_crypt
$algorithm = $1;
}
# Wide characters cause crypt to die
if (Bugzilla->params->{utf8})
{
utf8::encode($password) if utf8::is_utf8($password);
}
my $crypted_password;
if (!$algorithm)
{
# Wide characters cause crypt to die
if (Bugzilla->params->{utf8})
{
utf8::encode($password) if utf8::is_utf8($password);
}
# Crypt the password.
$crypted_password = crypt($password, $salt);

View File

@ -80,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\$";

View File

@ -12,6 +12,7 @@ use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla;
use Bugzilla::Comment;
use Bugzilla::Constants;
use Bugzilla::Error;
@ -20,7 +21,7 @@ use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate user_to_hash);
use Bugzilla::Bug;
use Bugzilla::BugMail;
use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural);
use Bugzilla::Util qw(trick_taint trim diff_arrays detaint_natural Dumper);
use Bugzilla::Version;
use Bugzilla::Milestone;
use Bugzilla::Status;
@ -614,7 +615,7 @@ sub update {
# We skip certain fields because their set_ methods actually use
# the external names instead of the internal names.
$params = Bugzilla::Bug::map_fields($params,
$params = Bugzilla::Bug::map_fields($params,
{ summary => 1, platform => 1, severity => 1, url => 1 });
my $ids = delete $params->{ids};
@ -623,11 +624,7 @@ sub update {
my @bugs = map { Bugzilla::Bug->check_for_edit($_) } @$ids;
my %values = %$params;
$values{other_bugs} = \@bugs;
if (exists $values{comment} and exists $values{comment}{comment}) {
$values{comment}{body} = delete $values{comment}{comment};
}
my $comment = delete $values{comment};
# Prevent bugs that could be triggered by specifying fields that
# have valid "set_" functions in Bugzilla::Bug, but shouldn't be
@ -648,6 +645,13 @@ sub update {
foreach my $bug (@bugs) {
$bug->set_all(\%values);
if ($comment) {
$bug->add_comment($comment->{body} || $comment->{comment}, {
isprivate => $comment->{is_private},
type => $comment->{type} || CMT_NORMAL,
work_time => $comment->{work_time},
});
}
if ($flags) {
my ($old_flags, $new_flags) = extract_flags($flags, $bug);
$bug->set_flags($old_flags, $new_flags);
@ -661,9 +665,7 @@ sub update {
}
$dbh->bz_commit_transaction();
foreach my $bug (@bugs) {
$bug->send_changes($all_changes{$bug->id});
}
Bugzilla->send_mail;
my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
# This doesn't normally belong in FIELD_MAP, but we do want to translate
@ -732,7 +734,7 @@ sub create {
$dbh->bz_commit_transaction();
$bug->send_changes();
Bugzilla->send_mail;
return { id => $self->type('int', $bug->bug_id) };
}
@ -837,7 +839,7 @@ sub add_attachment {
$_->bug->update($timestamp) foreach @created;
$dbh->bz_commit_transaction();
$_->send_changes() foreach @bugs;
Bugzilla->send_mail;
my @created_ids = map { $_->id } @created;
@ -950,9 +952,10 @@ sub update_attachment {
# Email users about the change
foreach my $bug (values %bugs) {
$bug->update();
$bug->send_changes();
}
Bugzilla->send_mail;
# Return the information to the user
return { attachments => \@result };
}
@ -977,14 +980,16 @@ sub add_comment {
$params->{is_private} = delete $params->{private};
}
# Append comment
my $info = {};
$bug->add_comment($comment, { isprivate => $params->{is_private},
work_time => $params->{work_time} });
$bug->update();
work_time => $params->{work_time},
type => $params->{type} || CMT_NORMAL });
$bug->update(undef, $info);
my $new_comment_id = $bug->{added_comments}[0]->id;
my $new_comment_id = $info->{added_comments}->[0]->{id};
# Send mail.
Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
Bugzilla->send_mail;
return { id => $self->type('int', $new_comment_id) };
}
@ -1012,7 +1017,7 @@ sub update_see_also {
$bug->add_see_also($_) foreach @$add;
}
}
my %changes;
foreach my $bug (@bugs) {
my $change = $bug->update();
@ -1026,10 +1031,10 @@ sub update_see_also {
# We still want a changes entry, for API consistency.
$changes{$bug->id}->{see_also} = { added => [], removed => [] };
}
Bugzilla::BugMail::Send($bug->id, { changer => $user });
}
Bugzilla->send_mail;
return { changes => \%changes };
}

View File

@ -76,20 +76,12 @@ sub type {
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 );
}
while ( read( 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};

View File

@ -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(.);

View File

@ -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.

View File

@ -183,25 +183,15 @@ 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})
|| $ARGS->{serverpush};
&& $ENV{HTTP_USER_AGENT} =~ /Mozilla.[3-9]/
&& $ENV{HTTP_USER_AGENT} !~ /compatible|msie|webkit|like\s*gecko/i
&& !$agent && !defined($ARGS->{serverpush})
|| $ARGS->{serverpush};
# The params object to use for the actual query itself
my $params;
@ -717,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 = {};
@ -773,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();
@ -816,15 +800,18 @@ if (@bugidlist)
}
# Compute percentage complete without rounding.
my $sum = $time_info->{work_time} + $time_info->{remaining_time};
if ($sum > 0)
if (exists $total_info->{percentage_complete})
{
$time_info->{percentage_complete} = 100*$time_info->{work_time}/$sum;
}
else
{
# remaining_time <= 0
$time_info->{percentage_complete} = 0
my $sum = $total_info->{work_time} + $total_info->{remaining_time};
if ($sum > 0)
{
$total_info->{percentage_complete} = 100*$total_info->{work_time}/$sum;
}
else
{
# remaining_time <= 0
$total_info->{percentage_complete} = 0
}
}
################################################################################
@ -887,7 +874,7 @@ $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};
@ -991,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)

View File

@ -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

View File

@ -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>

View File

@ -1,5 +0,0 @@
1
include .
exclude data/params.bugs3
exclude data/params.bugs3-sm
exclude data/hgsvn-filemap

View File

@ -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&amp;bug_status=ASSIGNED&amp;bug_status=REOPENED&amp;email1=%userid%&amp;emailtype1=exact&amp;emailassigned_to1=1&amp;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/'
);

View File

@ -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&amp;bug_status=ASSIGNED&amp;bug_status=REOPENED&amp;email1=%userid%&amp;emailtype1=exact&amp;emailassigned_to1=1&amp;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/'
);

View File

@ -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);
@ -320,6 +324,8 @@ if ($action eq 'remove_regexp')
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);

View File

@ -189,21 +189,25 @@ 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);
if (!$user->in_group('editcomponents'))
# Create groups and series for the new product, if requested.
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
$product->_create_bug_group(1);
# group the right to manage his newly created product (or admin group is explicitly requested)
$product->_create_bug_group(1, $new_bug_group);
}
# Create groups and series for the new product, if requested.
$product->_create_bug_group() if $ARGS->{makeproductgroup};
$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)
@ -375,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)
@ -468,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;

View File

@ -338,6 +338,8 @@ 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 ];

View File

@ -73,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;

73
email_in.cgi Executable file
View File

@ -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;
}

View File

@ -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__

View File

@ -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+'&amp;cloned_comment='+num+'">other</a>';
s += '/<a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&amp;product='+encodeURIComponent(bug_info.product)+'&amp;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+'&amp;product='+encodeURIComponent(bug_info.extprod)+'&amp;cloned_comment='+num+'">ext</a>';
else if (bug_info.intprod)
s += '/<a href="enter_bug.cgi?cloned_bug_id='+bug_info.id+'&amp;product='+encodeURIComponent(bug_info.intprod)+'&amp;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;
}

View File

@ -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 = '';

View File

@ -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)
{

View File

@ -51,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)
@ -109,9 +110,11 @@ $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),
@ -129,6 +132,11 @@ if ($action eq "wrap")
$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);
}
elsif ($action eq "plot")

View File

@ -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}) ||

View File

@ -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,

View File

@ -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; }

View File

@ -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

View File

@ -61,3 +61,7 @@
vertical-align: middle;
min-width: 8em;
}
table.report td {
min-width: 2em;
}

View File

@ -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;
}

View File

@ -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 %]" />

View File

@ -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 %]

View File

@ -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 %]

View File

@ -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.",
}
%]

View File

@ -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 " _

View File

@ -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.",
} %]

View File

@ -58,6 +58,13 @@
<b><label for="makeproductgroup">Create access group for this product</label></b>
</td>
</tr>
<tr>
<td colspan="2">
<input type="checkbox" id="makeadmingroup" name="makeadmingroup" value="1"
[% ' checked="checked"' IF Param('makeproductgroups') %] />
<b><label for="makeadmingroup">Create administration group for this product</label></b>
</td>
</tr>
</table>
<input type="submit" value="Add" />

View File

@ -226,7 +226,7 @@ var close_status_array = [
[% END %]
[% INCLUDE custom_fields style = "width: 40em"
cf = Bugzilla.active_custom_fields({ 'type' => constants.FIELD_TYPE_TEXTAREA }) %]
cf = Bugzilla.active_custom_fields({ 'type' => [ constants.FIELD_TYPE_TEXTAREA, constants.FIELD_TYPE_EAV_TEXTAREA ] }) %]
[% INCLUDE custom_fields style = "min-width: 20em; max-width: 40em"
cf = Bugzilla.active_custom_fields({ 'type' => constants.FIELD_TYPE_MULTI_SELECT }) %]

View File

@ -55,6 +55,7 @@
bug.id && field.type != constants.FIELD_TYPE_BUG_ID_REV &&
field.type != constants.FIELD_TYPE_MULTI_SELECT &&
field.type != constants.FIELD_TYPE_TEXTAREA &&
field.type != constants.FIELD_TYPE_EAV_TEXTAREA &&
field.type != constants.FIELD_TYPE_BUG_URLS &&
field.type != constants.FIELD_TYPE_BUG_ID %]
@ -259,6 +260,15 @@
cols = 30
defaultcontent = value
tabindex = tabindex %]
[% CASE constants.FIELD_TYPE_EAV_TEXTAREA %]
[% INCLUDE global/textarea.html.tmpl
id = field.name
name = field.name
minrows = 4
maxrows = 8
cols = 30
defaultcontent = value
tabindex = tabindex %]
[% CASE constants.FIELD_TYPE_BUG_URLS %]
[% '<ul class="bug_urls">' IF value.size %]
[% FOREACH url = value %]
@ -292,7 +302,7 @@
[% IF show_search_link %]
</td><td style="padding-left: 5px">[% PROCESS search_link %](search)</a></td></tr></table>
[% END %]
[% ELSIF field.type == constants.FIELD_TYPE_TEXTAREA %]
[% ELSIF field.type == constants.FIELD_TYPE_TEXTAREA || field.type == constants.FIELD_TYPE_EAV_TEXTAREA %]
<div class="uneditable_textarea">[% value | html | wrap_comment %]</div>
[% ELSIF field.type == constants.FIELD_TYPE_BUG_ID %]
[% IF bug.${field.name} %]

View File

@ -6,6 +6,8 @@
[% PROCESS "bug/show-header.html.tmpl" %]
[% Hook.process('bug_link_hook') %]
[% PROCESS bug/navigate.html.tmpl %]
[% PROCESS bug/edit.html.tmpl %]

View File

@ -532,6 +532,10 @@
Can't use [% field FILTER html %] as a field name.
[% END %]
[% BLOCK error_invalid_fulltext_query %]
The query text you specified cannot be handled by Bugzilla full-text search engine.
[% END %]
[% BLOCK error_in_search_needs_bugid_field %]
Subquery search operator ("In Search Results") can only be applied to fields of type "Bug ID".
"[% field | html %]" is not a Bug ID field.
@ -1598,6 +1602,19 @@
"[% suggested | html %]"?
[% END %]
[% BLOCK error_product_mandatory_group_required %]
Bugzilla group security policy requires that all products have at least
one group which is set as MANDATORY/MANDATORY, which means that only users of
this group or users explicitly added as CC/Assignee/QA may access bugs in
the corresponding product.
[% END %]
[% BLOCK error_product_entry_group_required %]
[% terms.Bugzilla %] group security policy requires that all products have at least
one group which is set as ENTRY, which means that only users of
this group may file new bugs in the corresponding product.
[% END %]
[% BLOCK error_product_name_already_in_use %]
[% title = "Product name already exists" %]
[% admindocslinks = {'products.html' => 'Administering products'} %]

View File

@ -225,6 +225,10 @@
[% ELSIF field_obj.type == constants.FIELD_TYPE_NUMERIC %]
[%# Remove trailing zeros %]
[% bug.$column.replace('((\.\d*[1-9])|\.)0+$', '$2') %]
[% ELSIF field_obj.type == constants.FIELD_TYPE_TEXTAREA ||
field_obj.type == constants.FIELD_TYPE_EAV_TEXTAREA %]
[%- v = bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) -%]
[%- v | quoteUrls(bug.bug_id) | wrap_comment -%]
[% ELSIF column == 'work_time' ||
column == 'remaining_time' ||
column == 'estimated_time' ||
@ -267,7 +271,8 @@
[%- bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
</a>
[% ELSE %]
[%- bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
[%- v = bug.$column.truncate(abbrev.$column.maxlength, abbrev.$column.ellipsis) FILTER html -%]
[%- v.replace("\n", '<br>') -%]
[% END %]
</td>
[% END %]
@ -277,7 +282,7 @@
</tr>
[% IF loop.last() && time_info.time_present == 1 %]
[% IF loop.last() && total_info %]
[% PROCESS time_summary_line %]
[% END %]
@ -319,22 +324,18 @@
[% columns_to_span = columns_to_span + 1 %]
[% END %]
[% FOREACH column = displaycolumns %]
[% IF column == 'work_time' ||
column == 'remaining_time' ||
column == 'estimated_time' ||
column == 'percentage_complete' ||
column == 'interval_time' %]
[% IF total_info.defined(column) %]
[% IF columns_to_span > 0 %]
<td class="bz_total bz_total_label" colspan="
[%- columns_to_span FILTER html %]"><b>Totals</b></td>
[% columns_to_span = 0 %]
[% END %]
[% IF column == 'percentage_complete' %]
<td class="bz_total">[% time_info.percentage_complete
<td class="bz_total">[% total_info.percentage_complete
FILTER format(abbrev.$column.format_value) FILTER html %]</td>
[% ELSE %]
<td class="bz_total">
[%- PROCESS formattimeunit time_unit=time_info.$column %]</td>
[%- PROCESS formattimeunit time_unit=total_info.$column %]</td>
[% END %]
[% ELSIF columns_to_span == 0 %] [%# A column following the first total %]
<td class="bz_total">&nbsp;</td>

View File

@ -15,6 +15,14 @@
<script type="text/javascript" src="[% 'js/resize-iframe.js' | ts_url %]"></script>
</head>
<body>
[% FOREACH tbl = tbl_names %]
[% IF tbl == "-total-" %]
[% tbl_disp = "Total" %]
[% ELSE %]
[% tbl_disp = tbl %]
[% END %]
[% PROCESS "reports/report-table.html.tmpl" %]
[% END %]
</body>
</html>

View File

@ -35,13 +35,16 @@
[% ELSE %]
[% tbl_disp = tbl %]
[% END %]
[% tbl_field_disp FILTER csv %]: [% tbl_disp FILTER csv %]
[% tbl_field_disp _ ": " _ tbl_disp | csv %]
[% END %]
[% IF row_field %]
[% IF col_field AND row_field %]
[% row_field_disp _ " / " _ col_field_disp | csv %]
[% ELSIF row_field %]
[% row_field_disp FILTER csv %]
[% ELSIF col_field %]
[% col_field_disp FILTER csv %]
[% END %]
[% " / " IF col_field AND row_field %]
[% col_field_disp FILTER csv %]
[% IF col_field -%]
[% FOREACH col = col_names -%]
[% colsepchar %]

View File

@ -53,37 +53,38 @@ END; %]
<h2>[% tbl_disp FILTER email FILTER html %]</h2>
[% END %]
<table>
<tr>
<td>
</td>
<td align="center">
<strong>[% col_field_disp FILTER html %]</strong>
</td>
</tr>
<tr>
<td valign="middle">
<strong>[% row_field_disp FILTER html %]</strong>
</td>
<td>
[% classes = [ [ "t1", "t2" ] , [ "t3", "t4" ] ] %]
[% col_idx = 0 %]
[% row_idx = 0 %]
[% mlist = (measure == 'times' ? [ 'etime', 'wtime', 'rtime' ] : [ measure ]) %]
<table border="1" style="border-collapse: collapse">
<table class="report" border="1" style="border-collapse: collapse">
[% IF col_field %]
<tr>
<td class="[% classes.$row_idx.$col_idx %]">
</td>
<td class="[% classes.$row_idx.$col_idx %]"><div style="position: relative; height: 100%">
<img style="position: absolute; left: 0; top: 0; width: 100%; height: 100%" src="skins/standard/global/diag.svg" />
<table>
<tr><td></td><td><strong>[% col_field_disp FILTER html %]</strong></td></tr>
<tr><td><strong>[% row_field_disp FILTER html %]</strong></td><td></td></tr>
</table>
</div></td>
[% FOREACH col = col_names %]
[% col_idx = 1 - col_idx %]
<td class="[% classes.$row_idx.$col_idx %]"[% ' colspan="3"' IF measure == 'times' %]>
[% PROCESS value_display value = col field = col_field %]
</td>
[% END %]
[% IF col_names.size > 1 %]
<td class="ttotal"[% ' colspan="3"' IF measure == 'times' %]>
Total
</td>
[% END %]
</tr>
[% ELSE %]
<tr>
<td class="[% classes.$row_idx.$col_idx %]">
<strong>[% row_field_disp FILTER html %]</strong>
</td>
<td class="ttotal"[% ' colspan="3"' IF measure == 'times' %]>
Total
</td>
@ -111,7 +112,7 @@ END; %]
<a href="[% urlbase %]&amp;
[% PROCESS value_url value=row field=row_field %]&amp;
[% PROCESS value_url value=col field=col_field %]">
[% data.$tbl.$col.$row.$m %]</a>
[% data.$tbl.$col.$row.$m | format("%.01f") %]</a>
[% END %]
</td>
[% END %]
@ -123,7 +124,7 @@ END; %]
<a href="[% urlbase %]&amp;
[% PROCESS value_url value=row field=row_field %]
[% "&amp;$col_vals" IF col_vals %]">
[% row_total.$m %]</a>
[% row_total.$m | format("%.01f") %]</a>
[% grand_total.$m = grand_total.$m + row_total.$m %]
</td>
[% END %]
@ -141,7 +142,7 @@ END; %]
<a href="[% urlbase %]&amp;
[% PROCESS value_url value=col field=col_field %]
[% "&amp;$row_vals" IF row_vals %]">
[% col_totals.$col_n.$m %]</a>
[% col_totals.$col_n.$m | format("%.01f") %]</a>
</td>
[% END %]
[% col_n = col_n + 1 %]
@ -152,7 +153,7 @@ END; %]
<strong>
<a href="[% urlbase %]
[% "&amp;$row_vals" IF row_vals %]
[% "&amp;$col_vals" IF col_vals %]">[% grand_total.$m %]</a>
[% "&amp;$col_vals" IF col_vals %]">[% grand_total.$m | format("%.01f") %]</a>
</strong>
</td>
[% END %]
@ -160,10 +161,6 @@ END; %]
</tr>
</table>
</td>
</tr>
</table>
[% BLOCK value_display %]
[% SET disp_value = value %]
[% IF field == 'assigned_to' OR field == 'reporter' OR field == 'qa_contact' %]

View File

@ -18,11 +18,5 @@
# Contributor(s): Gervase Markham <gerv@gerv.net>
#%]
[% FOREACH tbl = tbl_names %]
[% measure_descs = {
rtime => 'Remaining time'
etime => 'Estimated time'
wtime => 'Actual time'
count => "Number of $terms.bugs"
} %]
[% PROCESS "reports/report-table.csv.tmpl" %]
[% END %]

View File

@ -17,14 +17,14 @@
#
# Contributor(s): Gervase Markham <gerv@gerv.net>
#%]
[%# INTERFACE:
# col_field: string. Name of the field being plotted as columns.
# row_field: string. Name of the field being plotted as rows.
# tbl_field: string. Name of the field being plotted as tables.
# tbl_names: array. List of values for the field being plotted as tables.
# time: integer. Seconds since the epoch.
# data: <depends on format>. Data to plot.
# data: <depends on format>. Data to plot.
# format: string. Format of the individual reports.
# width: integer. For image charts, height of the image.
# height: integer. For image charts, width of the image.
@ -41,10 +41,9 @@
[% tbl_field_disp = field_descs.$tbl_field || tbl_field %]
[% col_field_disp = field_descs.$col_field || col_field %]
[% row_field_disp = field_descs.$row_field || row_field %]
[% switchbase = switchbase FILTER html %]
[% title = BLOCK %]
Report:
Report:
[% IF tbl_field %]
[% tbl_field_disp FILTER html %]
[% END %]
@ -58,14 +57,15 @@
[% time = time FILTER time('%Y-%m-%d %H:%M:%S') FILTER html %]
[% PROCESS global/header.html.tmpl
[% PROCESS global/header.html.tmpl
style = "
.t1 { background-color: #ffffff } /* white */
.t2 { background-color: #dfefff } /* light blue */
.t3 { background-color: #dddddd } /* grey */
.t4 { background-color: #c3d3ed } /* darker blue */
.t3 { background-color: #ffffff } /* white */
.t4 { background-color: #dfefff } /* light blue */
.ttotal { background-color: #cfffdf } /* light green */
"
style_urls = ['skins/standard/reports.css']
header_addl_info = time
%]
@ -73,9 +73,9 @@
<p>[% query FILTER html %]</p>
[% END %]
<div align="center">
<div>
[% FOREACH tbl = tbl_names %]
[% FOREACH tbl = tbl_names %]
[% IF tbl == "-total-" %]
[% tbl_disp = "Total" %]
[% ELSE %]
@ -88,7 +88,7 @@
[% IF tbl %]
<h2>[% tbl_disp FILTER email FILTER html %]</h2>
[% END %]
[% imageurl = BLOCK %]report.cgi?[% imagebase FILTER html %]&amp;format=
[% format FILTER url_quote %]&amp;ctype=png&amp;action=plot&amp;
[% IF tbl_field %]
@ -101,91 +101,87 @@
[% END %]
[% END %]
[% END %]
[% END %]width=[% width %]&amp;height=[% height %]
[% END %]width=[% width %]&amp;height=[% height %]
[% END %]
<img alt="Graphical report results" src="[% imageurl %]"
width="[% width %]" height="[% height %]">
[% END %]
<br />
[% END %]
[% END %]
<table>
<form method="GET" action="?">
<input type="hidden" name="action" value="wrap" />
[% FOR k = switchparams.keys.sort %]
<input type="hidden" name="[% k | html %]" value="[% switchparams.$k | html %]" />
[% END %]
<table class="admin_table">
<tr>
<th>
Format:
</th>
<td>
[% formats = [
{ name => "pie", description => "Pie" },
{ name => "bar", description => "Bar" },
{ name => "line", description => "Line" },
{ name => "table", description => "Table" },
{ name => "csv", description => "CSV" },
] %]
<select name="format" style="width: 100%; border: 0; margin-top: 2px">
[% FOR f = formats %]
<option value="[% f.name | html %]"[% IF f.name == format %] selected="selected"[% END %]>[% f.description | html %]</option>
[% END %]
</select>
</td>
</tr>
<tr>
<th>
Show:
</th>
<td>
<select name="measure" style="width: 100%; border: 0; margin-top: 2px">
[% FOR m = measure_descs.keys.sort %]
<option value="[% m %]"[% IF measure == m %] selected="selected"[% END %]>[% measure_descs.$m %]</option>
[% END %]
</select>
</td>
</tr>
[% IF format != "table" %]
<tr>
<td>
[% formats = [ { name => "pie", description => "Pie" },
{ name => "bar", description => "Bar" },
{ name => "line", description => "Line" },
{ name => "table", description => "Table" } ] %]
[% formaturl = "report.cgi?$switchbase&amp;width=$width" _
"&amp;height=$height&amp;action=wrap" %]
[% FOREACH other_format = formats %]
[% NEXT IF other_format.name == "pie" AND row_field AND col_field %]
[% UNLESS other_format.name == format %]
<a href="[% formaturl %]&amp;format=[% other_format.name %]&amp;measure=[% measure %]">
[% END %]
[% other_format.description FILTER html %]
[% "</a>" UNLESS other_format.name == format %] |
[% END %]
<a href="[% formaturl %]&amp;measure=[% measure %]&amp;ctype=csv&amp;format=table">CSV</a>
Width:
</td>
<td>
<input type="text" name="width" value="[% width %]" />
</td>
</tr>
<tr>
<td>
Height:
</td>
<td>
<input type="text" name="height" value="[% height %]" />
</td>
</tr>
[% END %]
<tr>
<td colspan="2" style="text-align: center">
<input type="submit" value="Apply Format" />
</td>
[% IF format != "table" %]
<td>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</td>
[% sizeurl = BLOCK %]report.cgi?
[% switchbase %]&amp;action=wrap&amp;format=[% format FILTER html %]&amp;measure=[% measure FILTER html %][% END %]
<td align="center">
<a href="[% sizeurl %]&amp;width=[% width %]&amp;height=
[% height + 100 %]">Taller</a><br />
<a href="[% sizeurl %]&amp;width=[% width - 100 %]&amp;height=
[% height %]">Thinner</a> *
<a href="[% sizeurl %]&amp;width=[% width + 100 %]&amp;height=
[% height %]">Fatter</a>&nbsp;&nbsp;&nbsp;&nbsp;<br />
<a href="[% sizeurl %]&amp;width=[% width %]&amp;height=
[% height - 100 %]">Shorter</a><br />
</td>
[% END %]
</tr>
</table>
<table><tr><td>
[% measure_descs = {
rtime => 'Remaining time'
etime => 'Estimated time'
wtime => 'Actual time'
times => 'Estimated/Actual/Remaining'
count => "Number of $terms.bugs"
} %]
[% FOR m = measure_descs.keys.sort %]
[% IF m != 'times' || format == 'table' %]
[%+ IF measure != m %]<a href="[% formaturl %]&amp;format=[% format %]&amp;measure=[% m %]">[% END %]
[% measure_descs.$m %]
[% IF measure != m %]</a>[% END %]
[% IF NOT loop.last %] | [% END %]
[% END %]
[% END %]
</td></tr></table>
<p>
[% IF format == "table" %]
<a href="query.cgi?[% switchbase %]&amp;format=report-table">Edit
this report</a>
[% ELSE %]
<a href="query.cgi?[% switchbase %]&amp;chart_format=
[% format %]&amp;format=report-graph&amp;cumulate=[% cumulate %]">
Edit this report
</a>
[% END %]
</form>
<div style="margin-top: 1em">
<input type="button" onclick="window.location.href='query.cgi?[% switchbase | html %]
[%- IF format == "table" -%]&amp;format=report-table
[%- ELSE -%]&amp;chart_format=[% format %]&amp;format=report-graph&amp;cumulate=[% cumulate %]
[%- END -%]'" value="Edit this report" />
[% IF saved_report_id %]
| <a href="report.cgi?action=del&amp;saved_report_id=[% saved_report_id FILTER uri %]&amp;token=
[%~ issue_hash_token(['delete_report', saved_report_id]) FILTER uri %]">Forget this report</a>
[%~ issue_hash_token(['delete_report', saved_report_id]) FILTER uri %]">Forget this report</a>
[% ELSE %]
<form method="get" action="report.cgi">
<form method="get" action="report.cgi" style="display: inline">
<input type="submit" id="remember" value="Remember report" /> as
<input type="hidden" name="query" value="[% switchbase %]&amp;format=[% format FILTER html %]&amp;action=wrap" />
<input type="hidden" name="action" value="add" />
@ -193,8 +189,8 @@
<input type="text" id="name" name="name" size="20" value="" maxlength="64" />
</form>
[% END %]
</p>
</div>
</div>
[% PROCESS global/footer.html.tmpl %]

View File

@ -243,9 +243,9 @@ function wt_user_blur()
ed.style.color = 'red';
document.getElementById('divide_other_bug_id').value = '';
}
document.getElementById('divide_other_bug_id_text').style.color = ed.value ? 'gray' : '';
document.getElementById('divide_other_bug_id_text_2').style.color = ed.value ? 'gray' : '';
document.getElementById('divide_other_bug_id').disabled = ed.value ? true : false;
document.getElementById('divide_other_bug_id_text').style.color = ed.value && ed.value != 'за всех участников' ? 'gray' : '';
document.getElementById('divide_other_bug_id_text_2').style.color = ed.value && ed.value != 'за всех участников' ? 'gray' : '';
document.getElementById('divide_other_bug_id').disabled = ed.value && ed.value != 'за всех участников' ? true : false;
}
function wt_user_focus()
{

View File

@ -30,7 +30,7 @@
<td align="center">
<form name="lastdaysform" action="fill-day-worktime.cgi" method="get">
<input value="Select last days" type="submit" />
<select name="lastdays">
<select [% lastdays <= 7 ? 'name="lastdays"' : 'style="display: none"' %] id="_lastdays" onchange="check_other(this)">
<option value="1" [% " selected=\"selected\"" IF (!lastdays)||(lastdays==1) %]>1</option>
<option value="2" [% " selected=\"selected\"" IF (lastdays==2) %]>2</option>
<option value="3" [% " selected=\"selected\"" IF (lastdays==3) %]>3</option>
@ -38,7 +38,9 @@
<option value="5" [% " selected=\"selected\"" IF (lastdays==5) %]>5</option>
<option value="6" [% " selected=\"selected\"" IF (lastdays==6) %]>6</option>
<option value="7" [% " selected=\"selected\"" IF (lastdays==7) %]>7</option>
<option value="other">other...</option>
</select>
<input [% lastdays > 7 ? 'name="lastdays" style="width: 80px"' : 'style="display: none; width: 80px"' %] value="[% lastdays || 1 %]" />
</form>
</td>
</tr>
@ -124,12 +126,29 @@
</td></tr></table>
</form>
<script>
function check_other(sel)
{
if (sel.style.display != 'none' && sel.value == 'other')
{
var txt = sel.nextSibling;
while (txt && txt.nodeName != 'INPUT')
txt = txt.nextSibling;
txt.setAttribute('name', sel.name);
txt.style.display = '';
sel.setAttribute('name', '');
sel.style.display = 'none';
}
}
check_other(document.getElementById('_lastdays'));
</script>
[% PROCESS global/footer.html.tmpl %]
[% BLOCK today_or_lastdays %]
[% IF lastdays>1 %]
Last [% lastdays %] Days
[% ELSE %]
Today
[% END %]
[% IF lastdays>1 %]
Last [% lastdays %] Days
[% ELSE %]
Today
[% END %]
[% END %]