Compare commits

...

133 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
Vitaliy Filippov ae75b4e493 Release v2016-09-01 2016-09-01 20:59:42 +03:00
Vitaliy Filippov 55bee6cdcc Only unescape correct UTF8 sequences in MediaWiki page sections 2016-09-01 20:58:25 +03:00
Vitaliy Filippov 13f6cca220 Fix WS.Group, add changelog 2016-09-01 15:34:13 +03:00
Vitaliy Filippov 45686f41a3 Fix total summing for images 2016-08-03 13:31:05 +03:00
Vitaliy Filippov a8e11f918e Move report generation logic to Bugzilla::Report, support reports in whines 2016-08-03 00:32:35 +03:00
Vitaliy Filippov ad5c5b6814 Pass work_time to create attachment form 2016-07-27 15:14:55 +03:00
Vitaliy Filippov 76be373f40 Fix "column totals" and "grand totals" in table reports 2016-07-26 16:47:38 +03:00
Vitaliy Filippov d2646d08b5 Add ids into longdescs and bugs_activity views 2016-07-05 12:16:15 +03:00
Vitaliy Filippov 1db3507cbf Check saved search runner permissions correctly in _in_search() 2016-06-01 14:45:16 +03:00
Vitaliy Filippov e63bc93fec Do not lowercase <pre> content O_o 2016-04-14 19:15:54 +03:00
Vitaliy Filippov 7bf84a131b Do not check permissions for sharer's queries 2016-04-11 17:27:58 +03:00
Vitaliy Filippov 41cf047168 Enclose xml-invalid hash keys into attributes 2016-04-06 17:47:15 +03:00
Vitaliy Filippov 4b8bea7b17 Fix Bug.search api 2016-04-06 17:34:22 +03:00
Vitaliy Filippov 8e13a0ff60 Do not remove visibility values the user cannot see in editvalues 2016-03-09 17:26:07 +03:00
Vitaliy Filippov 3efbc41166 Treat uppercase "Now" also as undef 2016-03-09 16:57:12 +03:00
Vitaliy Filippov 885419afba Fix absolute urls in whine mail 2016-03-09 16:47:29 +03:00
Vitaliy Filippov c91aaaf113 Remove duplicated wiki link 2016-03-09 12:42:43 +03:00
Vitaliy Filippov 25a101df9c Add FIXME 2016-03-02 15:57:58 +03:00
Vitaliy Filippov 824a39678d Fix except_fields for single empty value 2016-03-02 15:19:58 +03:00
Vitaliy Filippov 7153084880 Add joined multi-select columns 2016-03-01 18:23:42 +03:00
Vitaliy Filippov 003913e3fc Support multiple "look for bug in" URLs 2016-02-29 00:40:22 +03:00
Vitaliy Filippov 0bc3e2599c do not die for empty values 2016-02-16 16:30:12 +03:00
Vitaliy Filippov 3ade1d0a8d Allow multiple values for the same except_field in checkers 2016-01-29 15:19:02 +03:00
Vitaliy Filippov ea2dfa1fe4 Remove zero required version 2016-01-27 19:13:49 +03:00
Vitaliy Filippov 83202248a4 Remove "attachment already obsolete" error, fix longdesclength argument check 2016-01-27 18:15:04 +03:00
Vitaliy Filippov b8f096bd42 Check field type for "in search" operators 2016-01-27 18:11:29 +03:00
Vitaliy Filippov a6ec66012c Fix checker user_id validator 2016-01-27 18:02:27 +03:00
Vitaliy Filippov ca5dde4d80 Fix config.cgi list() call 2016-01-26 13:43:23 +03:00
Vitaliy Filippov 4736a51e18 Allow admin users to see all saved queries 2016-01-25 18:27:54 +03:00
Vitaliy Filippov 4ad3e1d881 Add "bypass group" parameter to Checkers 2016-01-25 18:19:14 +03:00
Vitaliy Filippov 98b88de635 Fix deadline history logging 2016-01-22 17:22:18 +03:00
Vitaliy Filippov 5b4ce0bd96 Fix :: -> 2016-01-19 15:48:53 +03:00
Vitaliy Filippov cedb870a81 Use same buglist generation code in whine.pl and in buglist.cgi 2015-12-29 17:58:15 +03:00
Vitaliy Filippov b37cc0c1b3 Remove current user modification hack for Bugzilla::Search 2015-12-24 16:42:47 +03:00
Vitaliy Filippov 3e789c2a51 Fix buglist value urls to use fields instead of quicksearch 2015-12-22 15:56:11 +03:00
Vitaliy Filippov d460b74b8c Do not make extra empty paragraphs 2015-12-07 12:58:50 +03:00
Vitaliy Filippov 40dc73f692 Fix possible duplicate key error when adding a bookmark to saved searches 2015-12-07 12:50:01 +03:00
Vitaliy Filippov 40251a927a Fix group matching 2015-12-07 12:31:53 +03:00
Vitaliy Filippov 823c7afe49 Fix Bugzilla.Group member manage method 2015-12-04 17:37:56 +03:00
Vitaliy Filippov a718d113a6 Do not cumulate totals across tables 2015-11-25 18:43:24 +03:00
Vitaliy Filippov 8d9eb2b89a fix for single include/exclude_fields 2015-11-20 13:45:51 +03:00
Vitaliy Filippov 35df417ee1 Proper fix 2015-11-18 15:32:21 +03:00
Vitaliy Filippov a7bf3751bd Fix editusers message 2015-11-18 14:48:47 +03:00
Vitaliy Filippov e760a23ae2 Do not crash in Search when multiple values are passed to a single-value operator 2015-11-18 14:45:57 +03:00
Vitaliy Filippov 6e9ee91dcf Add Group.{add,remove}_{members,managers} web services
Also move user_group_map manipulation code into Bugzilla::Group
2015-11-16 18:20:23 +03:00
Vitaliy Filippov bd69dca4a1 Fix query parameters in whine.pl 2015-11-13 14:40:42 +03:00
Vitaliy Filippov 2412e88f67 Fix "change several bugs at once", remove FIXME in Object.pm 2015-11-11 16:33:35 +03:00
Vitaliy Filippov 240180bb05 Fix 2 more places where $bug->alias was assumed to return arrayref 2015-11-11 14:26:38 +03:00
Vitaliy Filippov 773d33e561 Add newer Bugzilla::Bug->get_activity() method for compatibility (fix WS Bug.history method) 2015-11-11 14:24:57 +03:00
Vitaliy Filippov 0252410899 Also clear 0000-00-00 deadlines 2015-11-10 15:25:44 +03:00
Vitaliy Filippov 57f6b13dd5 Fix clearing deadline 2015-11-10 15:22:18 +03:00
Vitaliy Filippov 142a9561b2 Untaint bug IDs when passing them to visible_bugs() 2015-11-05 14:45:00 +03:00
Vitaliy Filippov 673aada44e Workaround mystical taint bug in Bugzilla::Object 2015-11-05 14:45:00 +03:00
Vitaliy Filippov 6f1bb58668 Report errors that happen during XML-RPC processing 2015-11-05 14:44:43 +03:00
Vitaliy Filippov 711952b34c Silence CGI.pm warning by using newer 4.08+ multi_param method 2015-11-05 14:00:05 +03:00
Vitaliy Filippov 3e4cd49b2c Correctly detect IN_EVAL under HTTPServerSimple, fix preload under taint mode 2015-11-05 13:51:28 +03:00
Vitaliy Filippov 58346deb00 Fix "email" type for merged XML-RPC webservices, update Bugzilla WS and XMLRPC subclass 2015-11-05 13:32:53 +03:00
Vitaliy Filippov cb5b2f06e0 Fix Bug.comments method after merge (follow-up to 10988be74f) 2015-10-26 16:17:46 +03:00
Vitaliy Filippov a51d9a9deb Fix add product error 2015-10-26 16:12:22 +03:00
Vitaliy Filippov 805d0afc82 Fix sql syntax 2015-10-26 16:06:14 +03:00
Vitaliy Filippov 799a177c31 Fix User.login after merging WS (follow-up to 10988be74f) 2015-10-26 15:42:31 +03:00
Vitaliy Filippov 9be26744ed Add separate "Create products" permission 2015-10-26 15:11:28 +03:00
Vitaliy Filippov f39407bcab Always sort group inclusion and grant options 2015-10-26 13:10:08 +03:00
Vitaliy Filippov 801b4312e8 Do not cut product to 8 characters, wrap custom fields 2015-10-26 12:42:43 +03:00
Vitaliy Filippov 10988be74f Merge WebServices from Bugzilla 5.0.1 2015-10-23 16:05:16 +03:00
Vitaliy Filippov 594e5ee476 Remove prepare/execute from whine.pl, more code style 2015-10-14 18:16:41 +03:00
Vitaliy Filippov 3d748e36ec Code style for whine.pl 2015-10-13 19:10:05 +03:00
Vitaliy Filippov 400a4b1bed Do not fail to login back just after logging out (when ?logout=1 is still in URL) 2015-10-13 18:55:18 +03:00
Vitaliy Filippov e34e9158fb Fix whining (did not work at all) 2015-10-13 18:35:36 +03:00
Vitaliy Filippov bcec6967ee Fix empty classification list on product edit page after creation 2015-10-09 15:22:23 +03:00
Vitaliy Filippov a3ffc5a247 Show only bug groups on product group control page (same as in save procedure) 2015-10-05 18:20:55 +03:00
Vitaliy Filippov 5d7747401f Use edit template for creating classifications, use literal actions to remove taint 2015-10-05 18:05:09 +03:00
127 changed files with 8790 additions and 3298 deletions

View File

@ -737,11 +737,6 @@ sub validate_obsolete
ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars);
}
if ($attachment->isobsolete)
{
ThrowCodeError('attachment_already_obsolete', $vars);
}
push(@obsolete_attachments, $attachment);
}
return @obsolete_attachments;

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,10 +495,10 @@ sub update
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
# FIXME 'shift ||' is just a temporary hack until all updating happens inside this function
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
$delta_ts = $delta_ts || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
# You can't set these fields by hand
$self->{deadline} =~ s/\s+.*$//so;
$self->{delta_ts} = $delta_ts;
delete $self->{votes};
delete $self->{lastdiffed};
@ -498,7 +517,8 @@ sub update
if ($self->id)
{
($changes, $old_bug) = $self->SUPER::update(@_);
$old_bug->{deadline} =~ s/\s+.*$//so;
($changes, $old_bug) = $self->SUPER::update(undef, $old_bug);
}
else
{
@ -562,6 +582,9 @@ sub update
# Insert the values into the multiselect value tables
$self->save_multiselects($changes);
# Insert the values into satellite value tables
$self->save_eavs($changes);
# Save reversed bug_id fields
$self->save_reverse_bugid_fields($changes);
@ -605,6 +628,10 @@ sub update
$self->prepare_mail_results($changes);
# Remove obsolete internal variables.
if ($additional_changes)
{
$additional_changes->{added_comments} = $self->{added_comments};
}
delete $self->{_old_self};
delete $self->{added_comments};
delete $self->{edited_comments};
@ -749,7 +776,7 @@ sub check_default_values
push @gids, $gid;
}
}
$self->{groups_in} = \@gids;
$self->{groups_in} = Bugzilla::Group->new_from_list(\@gids);
}
$self->set('groups', [ map { $_->id } @{$self->groups_in} ]);
$self->{cc} = $self->component_obj->initial_cc if !$self->{cc};
@ -914,6 +941,7 @@ sub check_dependent_fields
}
# Check other fields for empty values
elsif (!$self->{$fn} || ($field_obj->type == FIELD_TYPE_FREETEXT ||
$field_obj->type == FIELD_TYPE_EAV_TEXTAREA ||
$field_obj->type == FIELD_TYPE_TEXTAREA) && $self->{$fn} =~ /^\s*$/so)
{
my $nullable = $field_obj->check_is_nullable($self);
@ -927,6 +955,7 @@ sub check_dependent_fields
}
}
if (!$nullable && (!$self->{$fn} || ($field_obj->type == FIELD_TYPE_FREETEXT ||
$field_obj->type == FIELD_TYPE_EAV_TEXTAREA ||
$field_obj->type == FIELD_TYPE_TEXTAREA) && $self->{$fn} =~ /^\s*$/so))
{
ThrowUserError('field_not_nullable', { field => $field_obj });
@ -1078,14 +1107,6 @@ sub _check_bug_status
}
$self->set('resolution', undef);
}
else
{
# Changing between closed statuses zeroes the remaining time.
if ($old_status && $new_status->id != $old_status->id && $self->remaining_time != 0)
{
$self->set('remaining_time', 0);
}
}
}
sub _check_resolution
@ -1439,6 +1460,31 @@ sub save_multiselects
}
}
sub save_eavs
{
my ($self, $changes) = @_;
my @eavs = Bugzilla->get_fields({ obsolete => 0, type => FIELD_TYPE_EAV_TEXTAREA });
foreach my $field (@eavs)
{
my $name = $field->name;
if (defined $self->{$name})
{
my $old = $self->{_old_self} && $self->{_old_self}->$name || '';
my $v = $self->{$name};
if ($v ne $old)
{
Bugzilla->dbh->do("DELETE FROM bug_$name WHERE bug_id=?", undef, $self->id);
if ($v ne '')
{
Bugzilla->dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?, ?)", undef, $self->id, $v);
}
$changes->{$name} = [ $old, $v ];
}
}
}
}
sub save_see_also
{
my ($self, $changes) = @_;
@ -1486,7 +1532,7 @@ sub save_dup_id
sub save_added_comments
{
my ($self, $changes) = @_;
my $dbh = Bugzilla->dbh;
delete $self->{comments} if @{$self->{added_comments} || []};
foreach my $comment (@{$self->{added_comments} || []})
{
@ -1502,7 +1548,8 @@ sub save_added_comments
$comment->{bug_when} = $self->{delta_ts} if !$comment->{bug_when} || $comment->{bug_when} gt $self->{delta_ts};
my $columns = join(',', keys %$comment);
my $qmarks = join(',', ('?') x keys %$comment);
Bugzilla->dbh->do("INSERT INTO longdescs ($columns) VALUES ($qmarks)", undef, values %$comment);
$dbh->do("INSERT INTO longdescs ($columns) VALUES ($qmarks)", undef, values %$comment);
$comment->{id} = $dbh->bz_last_key('longdescs', 'id');
if (0+$comment->{work_time} != 0)
{
# Log worktime
@ -1675,7 +1722,9 @@ sub _sync_fulltext
$sql = "UPDATE $table SET short_desc=$row->[0],".
" comments=$row->[1], comments_private=$row->[2] WHERE $id_field=".$self->id;
}
return $sph->do($sql);
my $r = eval { $sph->do($sql) };
if ($@) { warn $@; }
return $r;
}
# This is the correct way to delete bugs from the DB.
@ -1903,7 +1952,11 @@ sub _set_deadline
# Validate entered deadline
$date = trim($date);
return undef if !$date;
if (!$date || $date =~ /^0000-00-00/)
{
$self->{deadline} = undef;
return undef;
}
$date =~ s/\s+.*//s;
validate_date($date) || ThrowUserError('illegal_date', { date => $date, format => 'YYYY-MM-DD' });
@ -2150,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;
@ -3490,6 +3557,12 @@ sub ValidateTime
return $time;
}
sub get_activity
{
my $self = shift;
return GetBugActivity($self->id, @_);
}
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub GetBugActivity
@ -3623,7 +3696,9 @@ sub GetBugActivity
{
my $change = $operations[$i]{changes}[$j];
my $field = Bugzilla->get_field($change->{fieldname});
if ($change->{fieldname} eq 'longdesc' || $field->{type} eq FIELD_TYPE_TEXTAREA)
if ($change->{fieldname} eq 'longdesc' ||
$field->{type} == FIELD_TYPE_TEXTAREA ||
$field->{type} == FIELD_TYPE_EAV_TEXTAREA)
{
my $diff = Bugzilla::Diff->new($change->{removed}, $change->{added})->get_table;
if (!@$diff)
@ -4283,7 +4358,14 @@ sub get_value
elsif ($field->type == FIELD_TYPE_BUG_ID_REV)
{
$self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
"SELECT bug_id FROM bugs WHERE ".$field->value_field->name." = ".$self->id
"SELECT bug_id FROM bugs WHERE ".$field->value_field->name." = ?", undef, $self->id
);
return $self->{$attr};
}
elsif ($field->type == FIELD_TYPE_EAV_TEXTAREA)
{
($self->{$attr}) = Bugzilla->dbh->selectrow_array(
"SELECT value FROM bug_$attr WHERE bug_id=?", undef, $self->id
);
return $self->{$attr};
}
@ -4309,7 +4391,7 @@ sub fields
map { $_->name } Bugzilla->get_fields({
obsolete => 0,
custom => 1,
type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_ID_REV ],
type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_EAV_TEXTAREA, FIELD_TYPE_BUG_ID_REV ],
}),
);
Bugzilla::Hook::process('bug_fields', { fields => \@fields });
@ -4334,7 +4416,7 @@ sub _validate_attribute
# every DB column may be returned via an autoloaded accessor
(map { $_ => 1 } Bugzilla::Bug->DB_COLUMNS),
# multiselect, bug_id_rev fields
(map { $_->name => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_ID_REV ] })),
(map { $_->name => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_EAV_TEXTAREA, FIELD_TYPE_BUG_ID_REV ] })),
# get_object accessors
(map { $_->name.'_obj' => 1 } Bugzilla->get_fields({ type => [ FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT ] })),
};

View File

@ -270,7 +270,7 @@ sub param
# When we are just requesting the value of a parameter...
if (scalar(@_) == 1)
{
my @result = $self->SUPER::param(@_);
my @result = $self->SUPER::multi_param(@_);
# Also look at the URL parameters, after we look at the POST
# parameters. This is to allow things like login-form submissions

View File

@ -43,6 +43,7 @@ use constant DB_COLUMNS => (
'sql_code', # SQL code for query is cached here
'except_fields', # "Exception" fields - see CF_DENY above.
'triggers', # Triggers (bug changes) (requires CF_FREEZE & !CF_FATAL)
'bypass_group_id', # Group members of which may bypass this check
);
use constant NAME_FIELD => 'message';
use constant ID_FIELD => 'id';
@ -53,16 +54,11 @@ use constant REQUIRED_CREATE_FIELDS => qw(query_id message);
use constant VALIDATORS => {
query_id => \&_check_query_id,
flags => \&_check_flags,
bypass_group_id => \&_check_bypass_group_id,
user_id => \&_check_user_id,
};
use constant UPDATE_COLUMNS => (
'query_id',
'flags',
'message',
'sql_code',
'except_fields',
'triggers',
);
use constant UPDATE_COLUMNS => (grep { $_ ne 'id' && $_ ne 'user_id' } DB_COLUMNS);
# The check works by executing this SQL query with added bugs.bug_id=? condition.
# Rebuild and save SQL code in the DB, from under the superuser
@ -80,6 +76,7 @@ sub refresh_sql
params => http_decode_query($query->query),
fields => [ 'bug_id' ],
user => $query->user,
ignore_permissions => 1,
);
my $terms = Bugzilla::Search::simplify_expression([
'AND_MANY', { term => 'bugs.bug_id=?' },
@ -125,14 +122,9 @@ sub update
sub _check_query_id
{
my ($invocant, $value, $field) = @_;
my $q = Bugzilla::Search::Saved->check({ id => $value });
# This code allows to create predicates using searches shared by other users,
# but the UI doesn't allow it (yet?).
if ($q->user->id != Bugzilla->user->id &&
(!$q->shared_with_group || !Bugzilla->user->in_group($q->shared_with_group)))
{
ThrowUserError('query_access_denied', { query => $q });
}
my $q = Bugzilla::Search::Saved->check({ id => $value });
# Check if a named query is not a search query, but just an HTTP url
if ($q->query =~ /^[a-z][a-z0-9]*:/iso)
{
@ -141,6 +133,18 @@ sub _check_query_id
return $q->id;
}
sub _check_user_id
{
my ($invocant, $value, $field) = @_;
return Bugzilla->user->id;
}
sub _check_bypass_group_id
{
my ($invocant, $value, $field) = @_;
return $value ? Bugzilla::Group->check({ id => $value })->id : undef;
}
sub _check_flags
{
my ($invocant, $value, $field) = @_;
@ -153,6 +157,7 @@ sub query_id { $_[0]->{query_id} }
sub user_id { $_[0]->{user_id} }
sub message { $_[0]->{message} }
sub sql_code { $_[0]->{sql_code} }
sub bypass_group_id { $_[0]->{bypass_group_id} }
sub flags { $_[0]->{flags} }
# Specific flags from the bitfield
@ -214,8 +219,8 @@ sub user
return $self->{user};
}
sub set_query_id { $_[0]->set('query_id', Bugzilla::Search::Saved->check({ id => $_[1] })->id) }
sub set_user_id { $_[0]->set('user_id', Bugzilla::User->check({ userid => $_[1] })->id) }
sub set_query_id { $_[0]->set('query_id', $_[1]) }
sub set_user_id { $_[0]->set('user_id', $_[1]) }
sub set_flags { $_[0]->set('flags', $_[1]) }
sub set_message { $_[0]->set('message', $_[1]) }
sub set_sql_code { $_[0]->set('sql_code', $_[1]) }

View File

@ -53,11 +53,13 @@ sub check
my $sql = [];
my @bind;
my ($s, $i);
for (values %$all)
for my $checker (values %$all)
{
if (($_->flags & $mask) == $flags)
if (($checker->flags & $mask) == $flags &&
# Do not run checkers which may be bypassed by user based on his permissions
(!$checker->bypass_group_id || !Bugzilla->user->in_group_id($checker->bypass_group_id)))
{
$s = $_->sql_code;
$s = $checker->sql_code;
push @$sql, $s;
push @bind, $bug_id;
}
@ -180,24 +182,24 @@ sub filter_failed_checkers
my ($checkers, $changes, $bug) = @_;
# Filter failed checkers by changes
my @rc;
for (@$checkers)
for my $checker (@$checkers)
{
if ($_->triggers)
if ($checker->triggers)
{
# Skip triggers
push @rc, $_;
push @rc, $checker;
next;
}
my $e = $_->except_fields;
my $e = $checker->except_fields;
my $ok = 1;
if ($_->deny_all)
if ($checker->deny_all)
{
# Allow only changes of except_fields to except values
for (keys %$changes)
for my $field (keys %$changes)
{
# If the field is not listed in except_fields, OR
# if there is a specific value in except_fields and our one is not equal
if (!exists $e->{$_} || (defined $e->{$_} && $changes->{$_}->[1] ne $e->{$_}))
if (!exists $e->{$field} || (defined $e->{$field} && !grep { $_ eq $changes->{$field}->[1] } list($e->{$field})))
{
$ok = 0;
last;
@ -207,20 +209,20 @@ sub filter_failed_checkers
else
{
# Forbid changes of except_fields to except values
for (keys %$e)
for my $field (keys %$e)
{
# work_time_date is a special pseudo-field meaning addition of backdated worktime
# the value of this pseudo-field is the date before which it is forbidden to fix worktime
# for example except_fields={work_time_date=2010-09-01} means forbid fixing worktime
# for dates before 2010-09-01
if ($_ eq 'work_time_date')
if ($field eq 'work_time_date')
{
my $today_date = strftime('%Y-%m-%d', localtime);
my $min_backdate = $e->{$_} || $today_date;
my $min_backdate = $e->{$field} || $today_date;
my $min_comment_date;
foreach (@{$bug->{added_comments} || []})
foreach my $comment (@{$bug->{added_comments} || []})
{
my $cd = $_->{bug_when} || $today_date;
my $cd = $comment->{bug_when} || $today_date;
if (!$min_comment_date || $cd lt $min_comment_date)
{
$min_comment_date = $cd;
@ -232,14 +234,14 @@ sub filter_failed_checkers
last;
}
}
elsif ($changes->{$_} && (!defined $e->{$_} || $changes->{$_}->[1] eq $e->{$_}))
elsif ($changes->{$field} && (!defined $e->{$field} || grep { $_ eq $changes->{$field}->[1] } list($e->{$field})))
{
$ok = 0;
last;
}
}
}
push @rc, $_ unless $ok;
push @rc, $checker unless $ok;
}
@$checkers = @rc;
}
@ -380,7 +382,6 @@ sub install_before_final_checks
{
my ($args) = @_;
print "Refreshing Checkers SQL...\n" if !$args->{silent};
Bugzilla->request_cache->{user} = Bugzilla::User->super_user;
for (Bugzilla::Checker->get_all)
{
eval { $_->update };

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

@ -26,14 +26,8 @@ sub get_param_list
},
{
name => 'viewvc_url',
type => 't',
default => '',
},
{
name => 'git_url',
type => 't',
name => 'look_in_urls',
type => 'l',
default => '',
},

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
@ -194,7 +196,7 @@ use Cwd qw(abs_path);
# CONSTANTS
#
# Bugzilla version
use constant BUGZILLA_VERSION => "2014.10-beta";
use constant BUGZILLA_VERSION => "2016.09";
# These are unique values that are unlikely to match a string or a number,
# to be used in criteria for match() functions and other things. They start
@ -392,6 +394,8 @@ use constant FIELD_TYPE_KEYWORDS => 8;
use constant FIELD_TYPE_NUMERIC => 30;
use constant FIELD_TYPE_EXTURL => 31;
use constant FIELD_TYPE_BUG_ID_REV => 32;
use constant FIELD_TYPE_EAV_TEXTAREA => 33;
use constant FIELD_TYPE__MAX => 33;
use constant FLAG_VISIBLE => 0;
use constant FLAG_NULLABLE => -1;

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

@ -825,6 +825,7 @@ use constant ABSTRACT_SCHEMA => {
sql_code => {TYPE => 'LONGTEXT'},
except_fields => {TYPE => 'LONGBLOB'},
triggers => {TYPE => 'LONGBLOB'},
bypass_group_id=> {TYPE => 'INT4', REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'}},
],
INDEXES => [
checkers_query_id_idx => { FIELDS => ['query_id'] },
@ -1131,6 +1132,7 @@ use constant ABSTRACT_SCHEMA => {
sortkey => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => '0'},
onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
title => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"},
isreport => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
],
INDEXES => [
whine_queries_eventid_idx => ['eventid'],
@ -1324,6 +1326,15 @@ use constant MULTI_SELECT_VALUE_TABLE => {
bug_id_idx => {FIELDS => [qw(bug_id value_id)], TYPE => 'UNIQUE'},
],
};
use constant EAV_TEXTAREA_VALUE_TABLE => {
FIELDS => [
bug_id => {TYPE => 'INT4', NOTNULL => 1},
value => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"},
],
INDEXES => [
bug_id_idx => {FIELDS => [qw(bug_id)], TYPE => 'UNIQUE'},
],
};
#--------------------------------------------------------------------------

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} };
@ -34,7 +35,7 @@ sub _in_eval
my $in = -$IN_EVAL;
for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++)
{
$in--, last if $sub =~ /^Bugzilla::HTTPServerSimple/;
last if $sub =~ /^Bugzilla::HTTPServerSimple/;
last if $sub =~ /^ModPerl/;
if ($sub =~ /^\(eval\)/)
{
@ -66,12 +67,33 @@ sub _error_message
{
$cgivars->{$_} = $cgi->uploadInfo($cgivars->{$_}) if $cgi->upload($_);
}
$mesg .= Data::Dumper->Dump([$vars, $cgivars, { %ENV }], ['error_vars', 'cgi_params', 'env']);
$mesg .= Data::Dumper->Dump([remove_objects($vars), $cgivars, { %ENV }], ['error_vars', 'cgi_params', 'env']);
# ugly workaround for Data::Dumper's \x{425} unicode characters
$mesg =~ s/((?:\\x\{(?:[\dA-Z]+)\})+)/eval("\"$1\"")/egiso;
return $mesg;
}
sub remove_objects
{
my ($v) = @_;
if (blessed $v)
{
return "$v";
}
elsif (ref($v) eq 'HASH')
{
return { map { ($_ => remove_objects($v->{$_})) } keys %$v };
}
elsif (ref($v) eq 'ARRAY')
{
return [ map { remove_objects($_) } @$v ];
}
else
{
return $v;
}
}
sub throw
{
my $self = shift;

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 });
}
@ -551,7 +551,8 @@ sub can_tweak
return 1;
}
# Return valid values for this field, arrayref of Bugzilla::Field::Choice objects.
# Return valid values for this field, arrayref of Bugzilla::Field::Choice objects,
# filtered by the current user's permissions.
# Includes disabled values is $include_disabled == true
sub legal_values
{
@ -892,11 +893,8 @@ sub remove_from_db
$dbh->bz_drop_column('bugs', $name);
}
if ($self->is_select)
{
# Delete the table that holds the legal values for this field.
$dbh->bz_drop_field_tables($self);
}
# Delete the table that holds the legal values for this field.
$dbh->bz_drop_field_tables($self);
$self->set_visibility_values(undef);
$self->set_null_visibility_values(undef);
@ -943,24 +941,24 @@ sub touch
sub set_visibility_values
{
my $self = shift;
my ($value_ids) = @_;
$self->update_visibility_values(FLAG_VISIBLE, $value_ids);
my ($value_ids, $skip_invisible) = @_;
$self->update_visibility_values(FLAG_VISIBLE, $value_ids, $skip_invisible);
return $value_ids && @$value_ids;
}
sub set_null_visibility_values
{
my $self = shift;
my ($value_ids) = @_;
$self->update_visibility_values(FLAG_NULLABLE, $value_ids);
my ($value_ids, $skip_invisible) = @_;
$self->update_visibility_values(FLAG_NULLABLE, $value_ids, $skip_invisible);
return $value_ids && @$value_ids;
}
sub set_clone_visibility_values
{
my $self = shift;
my ($value_ids) = @_;
$self->update_visibility_values(FLAG_CLONED, $value_ids);
my ($value_ids, $skip_invisible) = @_;
$self->update_visibility_values(FLAG_CLONED, $value_ids, $skip_invisible);
return $value_ids && @$value_ids;
}
@ -992,7 +990,7 @@ sub clear_default_values
sub update_visibility_values
{
my $self = shift;
my ($controlled_value_id, $visibility_value_ids) = @_;
my ($controlled_value_id, $visibility_value_ids, $skip_invisible) = @_;
$visibility_value_ids ||= [];
my $vis_field = $self->flag_field($controlled_value_id);
if (!$vis_field)
@ -1014,6 +1012,16 @@ sub update_visibility_values
$h = $h->{null}->{$self->id} if $controlled_value_id == FLAG_NULLABLE;
$h = $h->{clone}->{$self->id} if $controlled_value_id == FLAG_CLONED;
$h = $h ? { %$h } : {};
if ($skip_invisible)
{
# Do not affect visibility values the user can't see
# so he can't damage other user's visibility values for the same field value
my $allowed = { map { $_->id => 1 } @{$vis_field->legal_values} };
for (keys %$h)
{
delete $h->{$_} if !$allowed->{$_};
}
}
my $add = [];
for (@$visibility_value_ids)
{
@ -1267,13 +1275,8 @@ sub create
# Create the database column that stores the data for this field.
$dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
}
if ($obj->is_select)
{
# Create the table that holds the legal values for this field.
$dbh->bz_add_field_tables($obj);
}
# 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
@ -413,8 +414,8 @@ sub set_sortkey { $_[0]->set('sortkey', $_[1]); }
sub set_visibility_values
{
my $self = shift;
my ($value_ids) = @_;
$self->field->update_visibility_values($self->id, $value_ids);
my ($value_ids, $skip_invisible) = @_;
$self->field->update_visibility_values($self->id, $value_ids, $skip_invisible);
delete $self->{visibility_values};
return $value_ids;
}

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

View File

@ -150,6 +150,7 @@ sub status { return $_[0]->{status}; }
sub setter_id { return $_[0]->{setter_id}; }
sub requestee_id { return $_[0]->{requestee_id}; }
sub creation_date { return $_[0]->{creation_date}; }
sub modification_date { return $_[0]->{modification_date}; }
###############################
#### Methods ####

View File

@ -373,6 +373,123 @@ sub remove_from_db
$dbh->bz_commit_transaction();
}
sub add_users
{
my $self = shift;
my ($users, $isbless) = @_;
return if !@$users;
add_user_groups([ map { { group => $self, user => $_ } } @$users ], $isbless);
}
sub remove_users
{
my $self = shift;
my ($users, $isbless) = @_;
return if !@$users;
remove_user_groups([ map { { group => $self, user => $_ } } @$users ], $isbless);
}
# Add members or blessers to this group
# Bugzilla::Group::add_user_groups([ { user => Bugzilla::User or int id, group => Bugzilla::Group }, ... ], $isbless = 0 or 1)
sub add_user_groups
{
shift if $_[0] eq __PACKAGE__;
my ($rows, $isbless) = @_;
return if !@$rows;
# Filter duplicates
my $g = {};
for my $row (@$rows)
{
$g->{int(ref $row->{user} ? $row->{user}->id : $row->{user})}->{int($row->{group}->id)} = $row;
}
$isbless = $isbless ? 1 : 0;
my $dbh = Bugzilla->dbh;
# Filter already existing members
for my $row (@{ $dbh->selectall_arrayref(
"SELECT user_id, group_id FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
join(', ', map {
my $uid = $_;
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } keys %{$g->{$uid}};
} keys %$g).") FOR UPDATE", undef
) || [] })
{
delete $g->{$row->[0]}->{$row->[1]};
delete $g->{$row->[0]} if !%{$g->{$row->[0]}};
}
return if !%$g;
# Apply update
$dbh->do(
"INSERT INTO user_group_map (user_id, group_id, grant_type, isbless) VALUES ".
join(', ', map {
my $uid = $_;
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } keys %{$g->{$uid}};
} keys %$g)
);
# Record profiles_activity entries
my $cur_userid = Bugzilla->user->id;
my $group_fldid = Bugzilla->get_field('bug_group')->id;
if (!$isbless)
{
# FIXME: should create profiles_activity entries for blesser changes.
$dbh->do(
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ".
join(', ', map {
my $uid = $_;
join(', ', map { "($uid, $cur_userid, NOW(), $group_fldid, '', ".$dbh->quote($_->{group}->name).")" } values %{$g->{$uid}})
} keys %$g)
);
}
}
# Remove members or blessers from this group - arguments same as in add_user_groups()
sub remove_user_groups
{
shift if $_[0] eq __PACKAGE__;
my ($rows, $isbless) = @_;
return if !@$rows;
# Filter duplicates
$isbless = $isbless ? 1 : 0;
my $dbh = Bugzilla->dbh;
# Remember group objects
my $g = { map { $_->{group}->id => $_->{group} } @$rows };
# Filter already deleted members
my $del = {};
for my $row (@{ $dbh->selectall_arrayref(
"SELECT user_id, group_id FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
join(', ', map {
my $uid = int(ref $_->{user} ? $_->{user}->id : $_->{user});
my $gid = int($_->{group}->id);
"($uid, $gid, ".GRANT_DIRECT.", $isbless)";
} @$rows).") FOR UPDATE", undef
) || [] })
{
push @{$del->{$row->[0]}}, $row->[1];
}
return if !%$del;
# Apply update
$dbh->do(
"DELETE FROM user_group_map WHERE (user_id, group_id, grant_type, isbless) IN (".
join(', ', map {
my $uid = $_;
map { "($uid, $_, ".GRANT_DIRECT.", $isbless)" } @{$del->{$uid}};
} keys %$del).")"
);
# Record profiles_activity entries
my $cur_userid = Bugzilla->user->id;
my $group_fldid = Bugzilla->get_field('bug_group')->id;
if (!$isbless)
{
# FIXME: should create profiles_activity entries for blesser changes.
$dbh->do(
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue) VALUES ".
join(', ', map {
my $uid = $_;
map { "($uid, $cur_userid, NOW(), $group_fldid, ".$dbh->quote($g->{$_}->name).", '')" } @{$del->{$uid}};
} keys %$del)
);
}
}
# Add missing entries in bug_group_map for bugs created while
# a mandatory group was disabled and which is now enabled again.
sub _enforce_mandatory

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

@ -94,6 +94,7 @@ use constant SYSTEM_GROUPS => (
{ name => 'editusers' },
{ name => 'creategroups' },
{ name => 'editclassifications' },
{ name => 'createproducts' },
{ name => 'editcomponents' },
{ name => 'editkeywords' },
{ name => 'editbugs', userregexp => '.*' },
@ -111,7 +112,7 @@ use constant SYSTEM_GROUPS => (
{
name => 'admin_index',
include => [
qw(tweakparams editusers editclassifications editcomponents creategroups
qw(tweakparams editusers editclassifications createproducts editcomponents creategroups
editfields editflagtypes editkeywords bz_canusewhines bz_editcheckers)
],
},
@ -162,22 +163,33 @@ sub update_settings
sub update_system_groups
{
my $dbh = Bugzilla->dbh;
foreach my $definition (SYSTEM_GROUPS)
{
my $exists = new Bugzilla::Group({ name => $definition->{name} });
my $grp = new Bugzilla::Group({ name => $definition->{name} });
$definition->{isbuggroup} = 0;
$definition->{description} = html_strip(Bugzilla->messages->{system_groups}->{$definition->{name}});
my $include = delete $definition->{include};
if (!$exists)
if (!$grp)
{
Bugzilla::Group->create($definition);
if ($include && @$include)
$grp = Bugzilla::Group->create($definition);
}
elsif ($definition->{name} ne 'admin_index')
{
# Always fix inclusions in admin_index
$include = undef;
}
if ($include && @$include)
{
my $cur = { map { $_ => 1 } @{$dbh->selectcol_arrayref(
'SELECT g.name FROM group_group_map gm, groups g'.
' WHERE g.id=gm.member_id AND gm.grantor_id=? AND gm.grant_type=0', undef, $grp->id
) || []} };
$include = [ grep { !$cur->{$_} } @$include ];
if (@$include)
{
$dbh->do(
'INSERT INTO group_group_map (member_id, grantor_id, grant_type)'.
' SELECT g.id, ai.id, 0 FROM groups ai, groups g WHERE ai.name=?'.
' AND g.name IN (\''.join("','", @$include).'\')', undef, $definition->{name}
' SELECT g.id, ?, 0 FROM groups g WHERE g.name IN (\''.join("','", @$include).'\')', undef, $grp->id
);
}
}

View File

@ -864,6 +864,10 @@ WHERE description LIKE \'%[CC:%]%\'');
$dbh->bz_add_column('versions', 'sortkey');
$dbh->bz_add_column('checkers', 'bypass_group_id');
$dbh->bz_add_column('whine_queries', 'isreport');
_move_old_defaults($old_params);
################################################################
@ -3519,7 +3523,7 @@ sub _populate_bugs_fulltext
$datasize += length $_;
}
my $s = "($_, ".join(', ', map { $sph->$quote($_) } @{$rows->{$_}})."), ";
if ($len + length $s >= $max_packet)
if ($len > 0 && $len + length $s >= $max_packet)
{
last;
}

View File

@ -66,9 +66,7 @@ sub REQUIRED_MODULES {
{
package => 'CGI.pm',
module => 'CGI',
# 3.51 fixes a security problem that affects Bugzilla.
# (bug 591165)
version => '3.51',
version => '4.08',
},
{
package => 'Digest-SHA',
@ -132,11 +130,6 @@ sub REQUIRED_MODULES {
module => 'Text::Wrap',
version => '2013.0426',
},
{
package => 'Text-TabularDisplay',
module => 'Text::TabularDisplay',
feature => 'Table formatting inside bug comments',
},
{
package => 'LWP-MediaTypes',
module => 'LWP::MediaTypes',

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',
@ -219,6 +222,7 @@ $Bugzilla::messages->{en} = {
tweakparams => 'Allows to <a href="editparams.cgi">change Parameters</a>.',
editusers => 'Allows to <a href="editusers.cgi">edit or disable users</a> and include/exclude them from <b>all</b> groups.',
creategroups => 'Allows to <a href="editgroups.cgi">create, destroy and edit groups</a>.',
createproducts => 'Allows to <a href="editproducts.cgi">create new products</a>.',
editclassifications => 'Allows to <a href="editclassifications.cgi">create, destroy and edit classifications</a>.',
editcomponents => 'Allows to <a href="editproducts.cgi">create, destroy and edit all products, components, versions and milestones</a>.',
editkeywords => 'Allows to <a href="editvalues.cgi?field=keywords">create, destroy and edit keywords</a>.',

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

@ -121,6 +121,10 @@ sub _init
$sql .= $dbh->FOR_UPDATE;
}
# O_o some versions of Perl have a very annoying bug:
# class constants like DB_COLUMNS become tainted and the following select dies...
trick_taint($sql);
$object = $dbh->selectrow_hashref($sql, undef, @values);
return $object;
}
@ -172,10 +176,8 @@ sub new_from_list
my @detainted_ids;
foreach my $id (@$id_list)
{
detaint_natural($id) || ThrowCodeError('param_must_be_numeric', {function => $class . '::new_from_list'});
# Too large integers make PostgreSQL crash (FIXME: That's very STRANGE?!!)
next if $id > MAX_INT_32;
push(@detainted_ids, $id);
detaint_natural($id) || ThrowCodeError('param_must_be_numeric', {function => $class . '::new_from_list', id => $id_list->[scalar @detainted_ids]});
push @detainted_ids, $id;
}
# We don't do $invocant->match because some classes have
# their own implementation of match which is not compatible
@ -284,7 +286,7 @@ sub _do_list_select
# for the caller. So we copy the array. It's safe to untaint because
# they're only used in placeholders here.
my @untainted = @{ $values || [] };
trick_taint($_) foreach @untainted;
trick_taint($_) foreach $sql, @untainted; # O_o another taint workaround
my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
bless ($_, $class) foreach @$objects;
return $objects;
@ -332,6 +334,7 @@ sub set_all
sub update
{
my $self = shift;
my (undef, $old_self) = @_;
my $dbh = Bugzilla->dbh;
my $table = $self->DB_TABLE;
@ -340,7 +343,7 @@ sub update
$dbh->bz_start_transaction();
# Use a copy of old object
my $old_self = $self->new($self->id);
$old_self ||= $self->new($self->id);
my $numeric = $self->NUMERIC_COLUMNS;
my $date = $self->DATE_COLUMNS;

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,15 +649,19 @@ use constant is_default => 0;
sub _create_bug_group
{
my $self = shift;
my ($create_admin_group, $normal_group) = @_;
my $dbh = Bugzilla->dbh;
my $group_name = $self->name;
my $group_name = ($create_admin_group ? 'admin-' : '') . $self->name;
my $i = 1;
while (new Bugzilla::Group({ name => $group_name }))
{
$group_name = $self->name . ($i++);
$group_name = ($create_admin_group ? 'admin-' : '') . $self->name . ($i++);
}
my $group_description = get_text('bug_group_description', { product => $self });
my $group_description = get_text(
$create_admin_group ? 'admin_group_description' : 'bug_group_description',
{ product => $self }
);
my $group = Bugzilla::Group->create({
name => $group_name,
@ -627,9 +671,27 @@ sub _create_bug_group
# Associate the new group and new product.
$dbh->do(
'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol)'.
' VALUES (?, ?, ?, ?)', undef, $group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA
'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol, editcomponents, entry)'.
' VALUES (?, ?, ?, ?, ?, ?)', undef, $group->id, $self->id,
($create_admin_group ? (0, 0, 1, 0) : (CONTROLMAPMANDATORY, CONTROLMAPMANDATORY, 0, 1))
);
# Grant current user permission to edit the new group and include him in it
$dbh->do(
'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) VALUES (?, ?, ?, ?), (?, ?, ?, ?)',
undef, Bugzilla->user->id, $group->id, 1, 0, Bugzilla->user->id, $group->id, 0, 0
);
# Allow admin group to grant normal group
if ($create_admin_group && $normal_group)
{
$dbh->do(
'INSERT INTO group_group_map (member_id, grantor_id, grant_type) VALUES (?, ?, ?)',
undef, $group->id, $normal_group->id, GROUP_BLESS
);
}
return $group;
}
sub _create_series
@ -1007,7 +1069,11 @@ sub flag_types
sub allows_unconfirmed { return $_[0]->{allows_unconfirmed}; }
sub description { return $_[0]->{description}; }
sub isactive { return $_[0]->{isactive}; }
sub is_active { return $_[0]->{isactive}; }
sub votesperuser { return $_[0]->{votesperuser}; }
sub maxvotesperbug { return $_[0]->{maxvotesperbug}; }
sub votestoconfirm { return $_[0]->{votestoconfirm}; }
sub votes_per_user { return $_[0]->{votesperuser}; }
sub max_votes_per_bug { return $_[0]->{maxvotesperbug}; }
sub votes_to_confirm { return $_[0]->{votestoconfirm}; }

View File

@ -104,6 +104,301 @@ sub check
return $report;
}
sub _get_names
{
my ($names, $isnumeric, $field) = @_;
# These are all the fields we want to preserve the order of in reports.
my $f = $field && Bugzilla->get_field($field);
if ($f && $f->is_select)
{
my $values = [ '', map { $_->name } @{ $f->legal_values(1) } ];
my %dup;
@$values = grep { exists($names->{$_}) && !($dup{$_}++) } @$values;
return $values;
}
elsif ($isnumeric)
{
# It's not a field we are preserving the order of, so sort it
# numerically...
sub numerically { $a <=> $b }
return [ sort numerically keys %$names ];
}
else
{
# ...or alphabetically, as appropriate.
return [ sort keys %$names ];
}
}
sub get_measures
{
my $cols = Bugzilla::Search->REPORT_COLUMNS();
my $descs = {
count => Bugzilla->messages->{field_descs}->{count},
times => Bugzilla->messages->{field_descs}->{times},
};
for my $f (keys %$cols)
{
if ($cols->{$f}->{numeric})
{
$descs->{$f} = $cols->{$f}->{title};
}
}
return $descs;
}
sub execute
{
my $class = shift;
my ($ARGS, $runner) = @_;
my $valid_columns = Bugzilla::Search->REPORT_COLUMNS();
my $field = {};
for (qw(x y z))
{
my $f = $ARGS->{$_.'_axis_field'} || '';
trick_taint($f);
if ($f)
{
if ($valid_columns->{$f})
{
$field->{$_} = $f;
}
else
{
ThrowCodeError("report_axis_invalid", {fld => $_, val => $f});
}
}
}
if (!keys %$field)
{
ThrowUserError("no_axes_defined");
}
my $width = $ARGS->{width} || 600;
my $height = $ARGS->{height} || 350;
if (defined($width))
{
(detaint_natural($width) && $width > 0)
|| ThrowCodeError("invalid_dimensions");
$width <= 2000 || ThrowUserError("chart_too_large");
}
if (defined($height))
{
(detaint_natural($height) && $height > 0)
|| ThrowCodeError("invalid_dimensions");
$height <= 2000 || ThrowUserError("chart_too_large");
}
# These shenanigans are necessary to make sure that both vertical and
# horizontal 1D tables convert to the correct dimension when you ask to
# display them as some sort of chart.
my $is_table;
if ($ARGS->{format} eq 'table' || $ARGS->{format} eq 'simple')
{
$is_table = 1;
if ($field->{x} && !$field->{y})
{
# 1D *tables* should be displayed vertically (with a row_field only)
$field->{y} = $field->{x};
delete $field->{x};
}
}
else
{
if (!Bugzilla->feature('graphical_reports'))
{
ThrowCodeError('feature_disabled', { feature => 'graphical_reports' });
}
if ($field->{y} && !$field->{x})
{
# 1D *charts* should be displayed horizontally (with an col_field only)
$field->{x} = $field->{y};
delete $field->{y};
}
}
my $measure_alias = {
etime => 'estimated_time',
rtime => 'remaining_time',
wtime => 'interval_time',
};
my $measures = {
count => 'count',
};
my $old_columns = { %{Bugzilla::Search->COLUMNS($runner)} };
# Trick Bugzilla::Search: replace report columns SQL + add '_count' column
# FIXME: Remove usage of global variable COLUMNS in search generation code
%{Bugzilla::Search->COLUMNS($runner)} = (%{Bugzilla::Search->COLUMNS($runner)}, %{Bugzilla::Search->REPORT_COLUMNS($runner)});
Bugzilla::Search->COLUMNS($runner)->{count}->{name} = '1';
my $columns = Bugzilla::Search->COLUMNS($runner);
for my $column (keys %$columns)
{
if ($columns->{$column}->{numeric})
{
$measures->{$column} = $column;
}
}
my $measure = $ARGS->{measure} || '';
$measure = $measure_alias->{$measure} || $measure;
# Check that $measure is available (+ etime/rtime/wtime is usable only in table mode)
if ($measure eq 'times' ? !$is_table : !$measures->{$measure})
{
$measure = 'count';
}
# If the user has no access to the measured column, reset it to 'count'
if (!Bugzilla::Search->COLUMNS($runner)->{$measure eq 'times' ? 'remaining_time' : $measures->{$measure}})
{
$measure = 'count';
}
# Validate the values in the axis fields or throw an error.
my %a;
my @group_by = grep { !($a{$_}++) } values %$field;
my @axis_fields = @group_by;
for ($measure eq 'times' ? qw(estimated_time remaining_time interval_time) : $measure)
{
push @axis_fields, $measures->{$_} unless $a{$measures->{$_}};
}
# Clone the params, so that Bugzilla::Search can modify them
my $search = new Bugzilla::Search(
fields => \@axis_fields,
params => { %$ARGS },
user => $runner,
);
my $query = $search->getSQL();
$query =
"SELECT ".
($field->{x} || "''")." x, ".
($field->{y} || "''")." y, ".
($field->{z} || "''")." z, ".
join(', ', map { "SUM($measures->{$_}) $_" } ($measure eq 'times' ? qw(etime rtime wtime) : $measure)).
" FROM ($query) _report_table GROUP BY ".join(", ", @group_by);
$::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';
my $results = Bugzilla->dbh->selectall_arrayref($query, {Slice=>{}});
# We have a hash of hashes for the data itself, and a hash to hold the
# row/col/table names.
my %data;
my %names;
# Read the bug data and count the bugs for each possible value of row, column and table.
#
# We detect a numerical field, and sort appropriately, if all the values are numeric.
my %isnumeric;
foreach my $group (@$results)
{
for (qw(x y z))
{
$isnumeric{$_} &&= ($group->{$_} =~ /^-?\d+(\.\d+)?$/o);
$names{$_}{$group->{$_}} = 1;
}
$data{$group->{z}}{$group->{x}}{$group->{y}} = $is_table ? $group : $group->{$measure};
}
my @tbl_names = @{_get_names($names{z}, $isnumeric{z}, $field->{z})};
my @col_names = @{_get_names($names{x}, $isnumeric{x}, $field->{x})};
my @row_names = @{_get_names($names{y}, $isnumeric{y}, $field->{y})};
# The GD::Graph package requires a particular format of data, so once we've
# gathered everything into the hashes and made sure we know the size of the
# data, we reformat it into an array of arrays of arrays of data.
push @tbl_names, "-total-" if scalar(@tbl_names) > 1;
my @image_data;
foreach my $tbl (@tbl_names)
{
my @tbl_data;
push @tbl_data, \@col_names;
foreach my $row (@row_names)
{
my @col_data;
foreach my $col (@col_names)
{
$data{$tbl}{$col}{$row} ||= {};
push @col_data, $data{$tbl}{$col}{$row};
if ($tbl ne "-total-")
{
# This is a bit sneaky. We spend every loop except the last
# building up the -total- data, and then last time round,
# we process it as another tbl, and push() the total values
# into the image_data array.
if ($is_table)
{
for my $m (keys %{$data{$tbl}{$col}{$row}})
{
next if $m eq 'x' || $m eq 'y' || $m eq 'z';
$data{"-total-"}{$col}{$row}{$m} += $data{$tbl}{$col}{$row}{$m};
}
}
else
{
$data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
}
}
}
push @tbl_data, \@col_data;
}
unshift @image_data, \@tbl_data;
}
# Below a certain width, we don't see any bars, so there needs to be a minimum.
if ($width && $ARGS->{format} eq "bar")
{
my $min_width = (scalar(@col_names) || 1) * 20;
if (!$ARGS->{cumulate})
{
$min_width *= (scalar(@row_names) || 1);
}
$width = $min_width;
}
my $vars = {};
$vars->{fields} = $field;
# for debugging
$vars->{query} = $query;
# We need to keep track of the defined restrictions on each of the
# axes, because buglistbase, below, throws them away. Without this, we
# get buglistlinks wrong if there is a restriction on an axis field.
$vars->{col_vals} = $field->{x} ? http_build_query({ $field->{x} => $ARGS->{$field->{x}} }) : '';
$vars->{row_vals} = $field->{y} ? http_build_query({ $field->{y} => $ARGS->{$field->{y}} }) : '';
$vars->{tbl_vals} = $field->{z} ? http_build_query({ $field->{z} => $ARGS->{$field->{z}} }) : '';
my $a = { %$ARGS };
delete $a->{$_} for qw(x_axis_field y_axis_field z_axis_field ctype format query_format measure), @axis_fields;
$vars->{buglistbase} = http_build_query($a);
$vars->{image_data} = \@image_data;
$vars->{data} = \%data;
$vars->{measure} = $measure;
$vars->{tbl_field} = $field->{z};
$vars->{col_field} = $field->{x};
$vars->{row_field} = $field->{y};
$vars->{col_names} = \@col_names;
$vars->{row_names} = \@row_names;
$vars->{tbl_names} = \@tbl_names;
$vars->{width} = $width;
$vars->{height} = $height;
$vars->{cumulate} = $ARGS->{cumulate} ? 1 : 0;
$vars->{x_labels_vertical} = $ARGS->{x_labels_vertical} ? 1 : 0;
%{Bugzilla::Search->COLUMNS($runner)} = %$old_columns;
return $vars;
}
1;
__END__

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
@ -594,6 +597,7 @@ sub STATIC_COLUMNS
}
elsif ($field->type == FIELD_TYPE_BUG_ID_REV)
{
# FIXME: Если "обратных" (например, внутренних) багов несколько - join'енные поля по internal bugs работать не будут
push @bugid_fields, $field;
$columns->{$id}->{name} = "(SELECT ".
$dbh->sql_group_concat("rev_$id.bug_id", "', '").
@ -613,9 +617,19 @@ sub STATIC_COLUMNS
" FROM ".$type->REL_TABLE.", $t WHERE $t.".$type->ID_FIELD."=".$type->REL_TABLE.".value_id".
" AND ".$type->REL_TABLE.".bug_id=bugs.bug_id)";
}
elsif ($field->type == FIELD_TYPE_EAV_TEXTAREA)
{
my $t = "bug_".$field->name;
$columns->{$id}->{name} = "$t.value";
$columns->{$id}->{joins} = [ "LEFT JOIN $t ON $t.bug_id=bugs.bug_id" ];
}
elsif ($bug_columns->{$id})
{
$columns->{$id}->{name} ||= "bugs.$id";
if ($field->type == FIELD_TYPE_NUMERIC)
{
$columns->{$id}->{numeric} = 1;
}
}
}
@ -645,6 +659,17 @@ sub STATIC_COLUMNS
sortkey => 1,
};
}
elsif ($subid eq 'assigned_to' || $subid eq 'reporter' || $subid eq 'qa_contact')
{
$columns->{$id.'_'.$subid} = {
name => "map_${id}_${subid}.login_name",
title => $field->description . ' ' . $subfield->description,
subid => $subid,
sortkey => 1,
trim_email => 1,
joins => [ @$join, "LEFT JOIN profiles AS map_${id}_${subid} ON bugs_${id}.${subid}=map_${id}_${subid}.userid" ],
};
}
elsif ($subid eq 'product' || $subid eq 'component' || $subid eq 'classification')
{
$columns->{$id.'_'.$subid} = {
@ -660,6 +685,17 @@ sub STATIC_COLUMNS
],
};
}
elsif ($subid eq 'deadline')
{
$columns->{$id.'_'.$subid} = {
name => $dbh->sql_date_format("bugs_$id.deadline", '%Y-%m-%d'),
raw_name => "bugs_$id.deadline",
title => $field->description . ' ' . $subfield->description,
subid => $subid,
sortkey => 1,
joins => [ @$join ],
};
}
elsif ($subfield->type == FIELD_TYPE_SINGLE_SELECT)
{
my $type = $subfield->value_type;
@ -672,6 +708,32 @@ sub STATIC_COLUMNS
sortkey => 1,
};
}
elsif ($subfield->type == FIELD_TYPE_MULTI_SELECT)
{
my $type = $subfield->value_type;
my $t = $type->DB_TABLE;
$columns->{$id.'_'.$subid} = {
name => "(SELECT ".
$dbh->sql_group_concat("$t.".$type->NAME_FIELD, "', '").
" FROM ".$type->REL_TABLE.", $t WHERE $t.".$type->ID_FIELD."=".$type->REL_TABLE.".value_id".
" AND ".$type->REL_TABLE.".bug_id=bugs_$id".".bug_id)",
title => $field->description . ' ' . $subfield->description,
joins => [ @$join ],
subid => $subid,
sortkey => 1,
};
}
elsif ($subfield->type == FIELD_TYPE_MULTI_SELECT)
{
my $t = 'bug_'.$subfield->name;
$columns->{$id.'_'.$subid} = {
name => "bugs_$id"."_$t.value",
title => $field->description . ' ' . $subfield->description,
joins => [ @$join, "LEFT JOIN $t bugs_$id"."_$t ON bugs_$id"."_$t.bug_id=bugs_$id.bug_id" ],
subid => $subid,
sortkey => 1,
};
}
}
}
@ -707,12 +769,14 @@ sub STATIC_COLUMNS
# Now only removes time-tracking fields
sub COLUMNS
{
my ($self, $user) = @_;
$user ||= Bugzilla->user;
my $cache = Bugzilla->rc_cache_fields;
return $cache->{columns} if $cache->{columns};
return $cache->{columns}->{$user->id || ''} if $cache->{columns} && $cache->{columns}->{$user->id || ''};
my $columns = { %{ STATIC_COLUMNS() } };
# Non-timetrackers shouldn't see any time-tracking fields
if (!Bugzilla->user->is_timetracker)
if (!$user->is_timetracker)
{
delete $columns->{$_} for keys %{TIMETRACKING_FIELDS()};
}
@ -724,51 +788,69 @@ sub COLUMNS
{
$hint = ' FORCE INDEX (longdescs_bug_id_idx)';
}
my $priv = (Bugzilla->user->is_insider ? "" : "AND ldc0.isprivate=0 ");
my $priv = ($user->is_insider ? "" : "AND ldc0.isprivate=0 ");
# Not using JOIN (it could be joined on bug_when=creation_ts),
# because it would require COALESCE to an 'isprivate' subquery
# for private comments.
$columns->{comment0}->{name} =
"(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
" ORDER BY ldc0.bug_when LIMIT 1)";
$columns->{lastcomment}->{name} =
"(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
" ORDER BY ldc0.bug_when DESC LIMIT 1)";
$columns->{comment0} = {
%{$columns->{comment0}},
name => "(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
" ORDER BY ldc0.bug_when LIMIT 1)",
};
$columns->{lastcomment} = {
%{$columns->{lastcomment}},
name => "(SELECT thetext FROM longdescs ldc0$hint WHERE ldc0.bug_id = bugs.bug_id $priv".
" ORDER BY ldc0.bug_when DESC LIMIT 1)",
};
# Last commenter and last comment time
my $login = 'ldp0.login_name';
if (!Bugzilla->user->id)
if (!$user->id)
{
$login = $dbh->sql_string_until($login, $dbh->quote('@'));
}
$columns->{lastcommenter}->{name} =
"(SELECT $login FROM longdescs ldc0$hint".
" INNER JOIN profiles ldp0 ON ldp0.userid=ldc0.who WHERE ldc0.bug_id = bugs.bug_id $priv".
" ORDER BY ldc0.bug_when DESC LIMIT 1)";
$priv = (Bugzilla->user->is_insider ? "" : "AND lct.isprivate=0 ");
$columns->{last_comment_time}->{name} =
"(SELECT MAX(lct.bug_when) FROM longdescs lct$hint WHERE lct.bug_id = bugs.bug_id $priv)";
$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} = {
%{$columns->{last_comment_time}},
name => "(SELECT MAX(lct.bug_when) FROM longdescs lct$hint WHERE lct.bug_id = bugs.bug_id $priv)",
};
# Hide email domain for anonymous users
$columns->{cc}->{name} = "(SELECT ".$dbh->sql_group_concat((Bugzilla->user->id
$columns->{cc}->{name} = "(SELECT ".$dbh->sql_group_concat(($user->id
? 'profiles.login_name'
: $dbh->sql_string_until('profiles.login_name', $dbh->quote('@'))), "','").
" cc FROM cc, profiles WHERE cc.bug_id=bugs.bug_id AND cc.who=profiles.userid)";
foreach my $col (qw(assigned_to reporter qa_contact))
if (!$user->id)
{
my $sql = "map_${col}.login_name";
$columns->{$col}->{name} = Bugzilla->user->id ? $sql : $columns->{$col.'_short'}->{name};
foreach my $col (keys %$columns)
{
if ($columns->{$col}->{trim_email})
{
$columns->{$col} = {
%{$columns->{$col}},
name => $dbh->sql_string_until($columns->{$col}->{name}, $dbh->quote('@')),
};
}
}
}
Bugzilla::Hook::process('buglist_columns', { columns => $columns });
return $cache->{columns} = $columns;
return $cache->{columns}->{$user->id || ''} = $columns;
}
sub REPORT_COLUMNS
{
my ($self, $user) = @_;
$user ||= Bugzilla->user;
my $cache = Bugzilla->request_cache;
return $cache->{report_columns} if defined $cache->{report_columns};
return $cache->{report_columns}->{$user->id || ''} if $cache->{report_columns} && $cache->{report_columns}->{$user->id || ''};
my $columns = { %{ COLUMNS() } };
my $columns = { %{ $self->COLUMNS($user) } };
# There's no reason to support reporting on unique fields.
my @no_report_columns = qw(
@ -804,11 +886,12 @@ sub REPORT_COLUMNS
}
}
return $cache->{report_columns} = $columns;
return $cache->{report_columns}->{$user->id || ''} = $columns;
}
# Fields that can be searched on for changes
# This is now used only by query.cgi
# Depends on current user
sub CHANGEDFROMTO_FIELDS
{
# creation_ts, longdesc, longdescs.isprivate, commenter are treated specially
@ -932,10 +1015,11 @@ sub FUNCTIONS
type => FIELD_TYPE_BUG_ID_REV,
obsolete => 0
});
my $date_fields = join '|', map { $_->name } Bugzilla->get_fields({
my @bugid_fields = Bugzilla->get_fields({ type => FIELD_TYPE_BUG_ID });
my $date_fields = join '|', ((map { $_->name } Bugzilla->get_fields({
type => FIELD_TYPE_DATETIME,
obsolete => 0
});
})), (map { $_->name.'_deadline' } @bugid_fields));
$FUNCTIONS = {
'blocked|dependson' => {
'*' => \&_blocked_dependson,
@ -1103,11 +1187,11 @@ sub init
my $H = $self->{params};
# $self->{user} = User under which the search will be ran
# Bugzilla->user = Just current user
# $self->{user} = User under which the search will be ran (current user by default)
$self->{user} ||= Bugzilla->user;
my $user = $self->{user};
my $dbh = Bugzilla->dbh;
my $columns = $self->{columns} = $self->COLUMNS($user);
my @specialchart;
@ -1310,13 +1394,13 @@ sub init
$self->{interval_who} = $override ? $H->{period_who} : $H->{chfieldwho};
for ($self->{interval_from}, $self->{interval_to})
{
$_ = $_ && $_ ne 'now' ? SqlifyDate($_) : undef;
$_ = $_ && lc $_ ne 'now' ? SqlifyDate($_) : undef;
}
for ($self->{interval_who})
{
$_ = $_ ? ($_ eq '%user%' ? $self->{user} : Bugzilla::User::match_name($_, 1)->[0]) : undef;
}
COLUMNS->{interval_time}->{name} =
$columns->{interval_time}->{name} =
"(SELECT COALESCE(SUM(ldtime.work_time),0) FROM longdescs ldtime".
" WHERE ldtime.bug_id=bugs.bug_id".
($self->{interval_from} ? " AND ldtime.bug_when >= ".$dbh->quote($self->{interval_from}) : '').
@ -1355,9 +1439,9 @@ sub init
}
# Reset relevance column
COLUMNS->{relevance}->{bits} = [];
COLUMNS->{relevance}->{joins} = [];
COLUMNS->{relevance}->{name} = '(0)';
$columns->{relevance}->{bits} = [];
$columns->{relevance}->{joins} = [];
$columns->{relevance}->{name} = '(0)';
# Read charts from form hash
my @charts;
@ -1458,7 +1542,7 @@ sub init
}
}
if (!$user->is_super_user)
if (!$self->{ignore_permissions})
{
# If there are some terms in the search, assume it's enough
# to select bugs and attach security terms without UNION (OR_MANY).
@ -1524,7 +1608,7 @@ sub init
# write the FROM clause.
foreach my $orderitem (@inputorder)
{
BuildOrderBy($special_order, $orderitem, \@orderby);
$self->BuildOrderBy($special_order, $orderitem, \@orderby);
}
# Now JOIN the correct tables in the FROM clause.
@ -1541,16 +1625,16 @@ sub init
my @sql_fields;
foreach my $field (@fields)
{
for my $t (@{ COLUMNS->{$field}->{joins} || [] })
for my $t (@{ $columns->{$field}->{joins} || [] })
{
push @supptables, $t if !grep { $_ eq $t } @supptables;
}
if (COLUMNS->{$field}->{name})
if ($columns->{$field}->{name})
{
my $alias = $field;
# Aliases cannot contain dots in them. We convert them to underscores.
$alias =~ s/\./_/g;
push @sql_fields, COLUMNS->{$field}->{name} . " AS $alias";
push @sql_fields, $columns->{$field}->{name} . " AS $alias";
}
else
{
@ -1564,6 +1648,146 @@ sub init
$self->{sql} = $query;
}
sub get_columns
{
my $class = shift;
my ($params, $user) = @_;
my $displaycolumns;
if (defined $params->{columnlist} && $params->{columnlist} ne 'all')
{
$displaycolumns = [ split(/[ ,]+/, $params->{columnlist}) ];
}
elsif (defined Bugzilla->cookies->{COLUMNLIST})
{
$displaycolumns = [ split(/ /, Bugzilla->cookies->{COLUMNLIST}) ];
}
else
{
# Use the default list of columns.
$displaycolumns = [ DEFAULT_COLUMN_LIST ];
}
# Figure out whether or not the user is doing a fulltext search. If not,
# we'll remove the relevance column from the lists of columns to display
# and order by, since relevance only exists when doing a fulltext search.
my $fulltext = $params->{content} ||
grep { $params->{$_} eq 'content' && /^field(\d+-\d+-\d+)/ && $params->{"value$1"} } keys %$params;
my $columns = $class->COLUMNS($user);
$_ = $class->COLUMN_ALIASES->{$_} || $_ for @$displaycolumns;
@$displaycolumns = grep($columns->{$_} && trick_taint($_), @$displaycolumns);
if (!$user->is_timetracker)
{
@$displaycolumns = grep { !TIMETRACKING_FIELDS->{$_} } @$displaycolumns;
}
# Remove the relevance column if the user is not doing a fulltext search.
if (grep('relevance', @$displaycolumns) && !$fulltext)
{
@$displaycolumns = grep($_ ne 'relevance', @$displaycolumns);
}
@$displaycolumns = grep($_ ne 'bug_id', @$displaycolumns);
# The bug ID is always selected because bug IDs are always displayed.
# Severity, priority, resolution and status are required for buglist
# CSS classes.
# Display columns are selected because otherwise we could not display them.
my $selectcolumns = { map { $_ => 1 } (qw(bug_id bug_severity priority bug_status resolution product), @$displaycolumns) };
# Make sure that the login_name version of a field is always also
# requested if the realname version is requested, so that we can
# display the login name when the realname is empty.
my @realname_fields = grep(/_realname$|_short$/, @$displaycolumns);
foreach my $item (@realname_fields)
{
my $login_field = $item;
$login_field =~ s/_realname$|_short$//;
$selectcolumns->{$login_field} = 1;
}
return ($displaycolumns, [ keys %$selectcolumns ]);
}
sub get_order
{
my $class = shift;
my ($params, $user) = @_;
my $order = $params->{order} || "";
# First check if we'll want to reuse the last sorting order; that happens if
# the order is not defined or its value is "reuse last sort"
if (!$order || $order =~ /^reuse/i)
{
if ($order = Bugzilla->cookies->{LASTORDER})
{
# Cookies from early versions of Specific Search included this text,
# which is now invalid.
$order =~ s/ LIMIT 200//;
}
else
{
$order = ''; # Remove possible "reuse" identifier as unnecessary
}
}
my $columns = $class->COLUMNS($user);
my $fulltext = $params->{content} ||
grep { $params->{$_} eq 'content' && /^field(\d+-\d+-\d+)/ && $params->{"value$1"} } keys %$params;
my $old_orders = {
'' => 'bug_status,priority,assigned_to,bug_id', # Default
'bug number' => 'bug_id',
'importance' => 'priority,bug_severity,bug_id',
'assignee' => 'assigned_to,bug_status,priority,bug_id',
'last changed' => 'delta_ts,bug_status,priority,assigned_to,bug_id',
};
my @invalid_fragments;
my @orderstrings;
if ($order)
{
# Convert the value of the "order" form field into a list of columns
# by which to sort the results.
if ($old_orders->{lc $order})
{
@orderstrings = split /,/, $old_orders->{lc $order};
}
else
{
trick_taint($order);
# A custom list of columns. Make sure each column is valid.
foreach my $fragment (split(/,/, $order))
{
$fragment = trim($fragment);
next unless $fragment;
my ($column_name, $direction) = Bugzilla::Search::split_order_term($fragment);
$column_name = Bugzilla::Search::translate_old_column($column_name);
# Special handlings for certain columns
next if $column_name eq 'relevance' && !$fulltext;
# If we are sorting by votes, sort in descending order if
# no explicit sort order was given.
if ($column_name eq 'votes' && !$direction)
{
$direction = "DESC";
}
if (exists $columns->{$column_name})
{
$direction = " $direction" if $direction;
push @orderstrings, "$column_name$direction";
}
else
{
push @invalid_fragments, $fragment;
}
}
}
}
$order = $old_orders->{''} if !$order;
return (\@orderstrings, \@invalid_fragments);
}
# Return query-wide equality operators
sub get_equalities
{
@ -1580,10 +1804,11 @@ sub sv_quote
# HTML search description, moved here from templates
sub search_description_html
{
my $self = shift;
my ($exp, $debug, $inner) = @_;
my $opdescs = Bugzilla->messages->{operator_descs};
my $fdescs = Bugzilla->messages->{field_descs};
$exp = $exp->{terms_without_security} if ref $exp eq 'Bugzilla::Search';
$exp = $self->{terms_without_security} if !$exp;
my $html = '';
if (ref $exp eq 'ARRAY')
{
@ -1593,7 +1818,7 @@ sub search_description_html
for my $i (1 .. $#$exp)
{
$html .= " <li class='_".lc($op)."'>$op</li>" if $i > 1;
$html .= ' <li>'.search_description_html($exp->[$i], $debug, 1).'</li>';
$html .= ' <li>'.$self->search_description_html($exp->[$i], $debug, 1).'</li>' if $exp->[$i];
}
$html .= '</ul>';
}
@ -1612,7 +1837,7 @@ sub search_description_html
if ($d->[0])
{
my $a = COLUMN_ALIASES->{$d->[0]} || $d->[0];
$html .= '<span class="search_field">'.html_quote(COLUMNS->{$a}->{title} || $fdescs->{$a} || $a).':</span>';
$html .= '<span class="search_field">'.html_quote($self->{columns}->{$a}->{title} || $fdescs->{$a} || $a).':</span>';
}
$html .= ' '.$opdescs->{not} if $neg;
$html .= ' '.$opdescs->{$d->[1]} if !SEARCH_HIDDEN_OPERATORS->{$d->[1]};
@ -1642,7 +1867,7 @@ sub search_description_html
if ($f && @$f)
{
$s = $opdescs->{desc_fields};
$s =~ s/\$1/sv_quote(join(', ', map { COLUMNS->{$_}->{title} || $_ } @$f))/es;
$s =~ s/\$1/sv_quote(join(', ', map { $self->{columns}->{$_}->{title} || $_ } @$f))/es;
push @l, $s;
}
$html .= join(', ', @l);
@ -1839,6 +2064,7 @@ sub pronoun
# order. That is, that we wanted "A DESC", not "A".
sub BuildOrderBy
{
my $self = shift;
my ($special_order, $orderitem, $stringlist, $reverseorder) = (@_);
my ($orderfield, $orderdirection) = split_order_term($orderitem);
@ -1865,15 +2091,15 @@ sub BuildOrderBy
{
# DESC on a field with non-standard sort order means
# "reverse the normal order for each field that we map to."
BuildOrderBy($special_order, $subitem, $stringlist,
$orderdirection =~ m/desc/i);
$self->BuildOrderBy($special_order, $subitem, $stringlist, $orderdirection =~ m/desc/i);
}
return;
}
# Aliases cannot contain dots in them. We convert them to underscores.
$orderfield =~ s/\./_/g if exists COLUMNS->{$orderfield};
push @$stringlist, trim($orderfield . ' ' . $orderdirection);
else
{
# Aliases cannot contain dots in them. We convert them to underscores.
$orderfield =~ s/\./_/g if exists $self->{columns}->{$orderfield};
push @$stringlist, trim($orderfield . ' ' . $orderdirection);
}
}
# Splits out "asc|desc" from a sort order item.
@ -1912,7 +2138,7 @@ sub translate_old_column
my $rc = Bugzilla->request_cache;
if (!$rc->{columns_by_sql_code})
{
my $col = COLUMNS();
my $col = Bugzilla::Search->COLUMNS;
$rc->{columns_by_sql_code} = {
map { $col->{$_}->{name} => $_ } grep { $col->{$_}->{name} } keys %$col
};
@ -1979,7 +2205,7 @@ sub run_chart
$self->{field} = COLUMN_ALIASES->{$self->{field}} if COLUMN_ALIASES->{$self->{field}};
# chart -1 is generated by other code above, not from the user-
# submitted form, so we'll blindly accept any values in chart -1
if (!COLUMNS->{$self->{field}} && $check_field_name)
if (!$self->{columns}->{$self->{field}} && $check_field_name)
{
ThrowUserError('invalid_field_name', { field => $self->{field} });
}
@ -1993,15 +2219,15 @@ sub run_chart
# already know about it), or it was in %chartfields, so it is
# a valid field name, which means that it's ok.
trick_taint($self->{field});
if (!ref $self->{value})
if (!ref $self->{value} || ref $self->{value} eq 'ARRAY')
{
$self->{quoted} = Bugzilla->dbh->quote($self->{value});
$self->{quoted} = Bugzilla->dbh->quote(ref $self->{value} ? $self->{value}->[0] : $self->{value});
trick_taint($self->{quoted});
}
if (COLUMNS->{$self->{field}}->{name})
if ($self->{columns}->{$self->{field}}->{name})
{
$self->{fieldsql} = COLUMNS->{$self->{field}}->{name};
if (my $j = COLUMNS->{$self->{field}}->{joins})
$self->{fieldsql} = $self->{columns}->{$self->{field}}->{name};
if (my $j = $self->{columns}->{$self->{field}}->{joins})
{
# Automatically adds table joins when converted to string
$self->{fieldsql} = bless [ $self->{fieldsql}, $j, $self->{supptables}, $self->{suppseen} ], 'Bugzilla::Search::Code';
@ -2157,7 +2383,7 @@ sub changed
# User is searching for a comment with specific text
$ld_term->{where} .= ' AND '.$dbh->sql_iposition("$ld.thetext", $added);
}
elsif ($f{'longdescs.isprivate'} && $self->user->is_insider)
elsif ($f{'longdescs.isprivate'} && $self->{user}->is_insider)
{
# Insider is searching for a comment with specific privacy
$ld_term->{where} .= " AND $ld.isprivate = ".($v->{value} ? 1 : 0);
@ -2373,6 +2599,12 @@ sub _content_matches
}
$text =~ s/((?<!\\)(?:\\\\)*)([$pattern_part])/$1\\$2/gs;
$text =~ s/(?<=[\s-])-(?=[\s-])/\\-/gso;
$text =~ s/\s+$//so;
if (!$text)
{
ThrowUserError('invalid_fulltext_query');
return;
}
$text = ($self->{user}->is_insider ? '@(short_desc,comments,comments_private) ' : '@(short_desc,comments) ') . $text;
if ($dbh->isa('Bugzilla::DB::Mysql') &&
Bugzilla->localconfig->{sphinxse_port})
@ -2381,7 +2613,8 @@ sub _content_matches
$text =~ s/;/\\;/gso;
$text =~ s/\\/\\\\/gso;
# Space is needed after $text so Sphinx doesn't escape ";"
$text = "$text ;mode=extended;limit=1000;fieldweights=short_desc,5,comments,1,comments_private,1";
my $maxm = Bugzilla->params->{sphinx_max_matches} || 1000;
$text = "$text ;mode=extended;limit=$maxm;maxmatches=$maxm;fieldweights=short_desc,5,comments,1,comments_private,1";
$self->{term} = {
table => "bugs_fulltext_sphinx $table",
where => "$table.query=".$dbh->quote($text),
@ -2389,9 +2622,9 @@ sub _content_matches
};
if (!$self->{negated})
{
push @{COLUMNS->{relevance}->{joins}}, "LEFT JOIN bugs_fulltext_sphinx $table ON $table.id=bugs.bug_id AND $table.query=".$dbh->quote($text);
push @{COLUMNS->{relevance}->{bits}}, "$table.weight";
COLUMNS->{relevance}->{name} = '('.join("+", @{COLUMNS->{relevance}->{bits}}).')';
push @{$self->{columns}->{relevance}->{joins}}, "LEFT JOIN bugs_fulltext_sphinx $table ON $table.id=bugs.bug_id AND $table.query=".$dbh->quote($text);
push @{$self->{columns}->{relevance}->{bits}}, "$table.weight";
$self->{columns}->{relevance}->{name} = '('.join("+", @{$self->{columns}->{relevance}->{bits}}).')';
}
}
else
@ -2407,8 +2640,8 @@ sub _content_matches
if (@$ids && !$self->{negated})
{
# Pass relevance (weight) values via an overlong (CASE ... END)
push @{COLUMNS->{relevance}->{bits}}, '(case'.join('', map { ' when bugs.bug_id='.$_->[0].' then '.$_->[1] } @$ids).' end)';
COLUMNS->{relevance}->{name} = '('.join("+", @{COLUMNS->{relevance}->{bits}}).')';
push @{$self->{columns}->{relevance}->{bits}}, '(case'.join('', map { ' when bugs.bug_id='.$_->[0].' then '.$_->[1] } @$ids).' end)';
$self->{columns}->{relevance}->{name} = '('.join("+", @{$self->{columns}->{relevance}->{bits}}).')';
}
}
return;
@ -2440,8 +2673,8 @@ sub _content_matches
# this adds more terms to the relevance sql.
if (!$self->{negated})
{
push @{COLUMNS->{relevance}->{bits}}, @terms[grep { $_&1 } 0..$#terms];
COLUMNS->{relevance}->{name} = $dbh->sql_fulltext_relevance_sum(COLUMNS->{relevance}->{bits});
push @{$self->{columns}->{relevance}->{bits}}, @terms[grep { $_&1 } 0..$#terms];
$self->{columns}->{relevance}->{name} = $dbh->sql_fulltext_relevance_sum($self->{columns}->{relevance}->{bits});
}
}
@ -2449,7 +2682,11 @@ sub _timestamp_compare
{
my $self = shift;
my $dbh = Bugzilla->dbh;
$self->{fieldsql} = 'bugs.'.$self->{field};
if ($self->{columns}->{$self->{field}}->{joins})
{
push @{$self->{supptables}}, @{$self->{columns}->{$self->{field}}->{joins}};
}
$self->{fieldsql} = $self->{columns}->{$self->{field}}->{raw_name} || 'bugs.'.$self->{field};
if ($self->{value} =~ /^[+-]?\d+[dhwmy]$/is)
{
$self->{value} = SqlifyDate($self->{value});
@ -2886,21 +3123,7 @@ sub _equals
}
else
{
$self->{term} = "$self->{fieldsql} = $self->{quoted}";
}
}
sub _notequals
{
my $self = shift;
if ($self->{value} eq '')
{
$self->{term} = "($self->{fieldsql} != $self->{quoted} AND $self->{fieldsql} IS NOT NULL)";
}
else
{
$self->{allow_null} = 1;
$self->{term} = "$self->{fieldsql} != $self->{quoted}";
$self->{term} = "($self->{fieldsql} = $self->{quoted} AND $self->{fieldsql} IS NOT NULL)";
}
}
@ -3117,21 +3340,32 @@ sub _in_search_results
{
my $self = shift;
my $v = $self->{value};
my $sharer = $self->{params}->{sharer_id};
my $sharer = $self->{params}->{sharer_id} || $self->{user}->id;
# Do not check permissions for sharer's queries
my $m = 'new';
if ($v =~ /^(.*)<([^>]*)>/so)
{
# Allow to match on shared searches via 'SearchName <user@domain.com>' syntax
$v = $1;
$sharer = Bugzilla::User::login_to_id(trim($2), THROW_ERROR);
my $new_sharer = Bugzilla::User::login_to_id(trim($2), THROW_ERROR);
# Check permissions if the specified user is not the query's sharer
$m = 'check' if $new_sharer != $sharer;
$sharer = $new_sharer;
}
my $query = Bugzilla::Search::Saved->check({
my $query = Bugzilla::Search::Saved->$m({
name => trim($v),
user => $sharer,
})->query;
runner => $self->{user},
});
if (!$query)
{
ThrowUserError('object_does_not_exist', { name => trim($v), user => $sharer, class => 'Bugzilla::Search::Saved' });
}
$query = $query->query;
my $search = new Bugzilla::Search(
params => http_decode_query($query),
fields => [ "bugs.bug_id" ],
user => Bugzilla->user,
user => $self->{user},
);
my $sqlquery = $search->bugid_query;
my $t = "ins_".$self->{sequence};
@ -3142,8 +3376,18 @@ sub _in_search_results
}
else
{
$self->{term}->{where} = "$self->{fieldsql} = $t.bug_id";
$self->{term}->{notnull_field} = "$t.bug_id";
my $f = Bugzilla->get_field($self->{field});
if ($f && ($f->type == FIELD_TYPE_BUG_ID || $f->type == FIELD_TYPE_BUG_ID_REV) ||
$self->{field} eq 'blocked' || $self->{field} eq 'dependson' || $self->{field} eq 'dup_id')
{
$self->{term}->{where} = "$self->{fieldsql} = $t.bug_id";
$self->{term}->{notnull_field} = "$t.bug_id";
$self->{term}->{supp} = [ @{$self->{supptables}} ];
}
else
{
ThrowUserError('in_search_needs_bugid_field', { field => $self->{field} });
}
}
}
@ -3188,7 +3432,7 @@ sub negate_expression
if (ref $q eq 'HASH')
{
$q->{neg} = !$q->{neg};
$q->{allow_null} = ($q->{allow_null} || 0) == 1 ? 0 : $q->{allow_null};
$q->{allow_null} = ($q->{allow_null} || 0) == 1 ? 0 : 1;
$q->{description}->[1] = NEGATE_ALL_OPERATORS->{$q->{description}->[1]} || $q->{description}->[1];
}
elsif (!ref $q)

View File

@ -102,10 +102,12 @@ sub new
sub check
{
my $class = shift;
my $search = $class->SUPER::check(@_);
my $user = Bugzilla->user;
my ($param) = @_;
my $search = $class->SUPER::check($param);
my $user = $param->{runner} || Bugzilla->user;
return $search if $search->user->id == $user->id;
if (!$search->shared_with_group || !$user->in_group($search->shared_with_group))
if (!$user->in_group('admin') &&
(!$search->shared_with_group || !$user->in_group($search->shared_with_group)))
{
ThrowUserError('missing_query', {
queryname => $search->name,

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} : lc($1 eq '' ? ($2 eq '' ? $3 : $2) : html_quote($1))/geiso;
$text =~ s/<pre>((?:(?>.+?)(?:<pre>(?1)<\/pre>)?)+)?<\/pre>|\s*(<\/?$block_tags>)\s*|(<\/?$safe_tags>)|([<>&\"])/$4 ? $q->{$4} : ($1 eq '' ? lc($2 eq '' ? $3 : $2) : html_quote($1))/geiso;
# Replace nowrap markers (\1\0\1)
$text =~ s/\x01\x00\x01(.*?)\x01\x00\x01/<div style="white-space: nowrap">$1<\/div>/gso;
# Replace tables
$text = makeTables($text);
# Color quoted text
$text = makeCitations($text);
@ -422,10 +422,15 @@ sub unquote_wiki_url
my $anchor = '';
if ($article =~ s/#(.*)$//so)
{
my $a;
$anchor = $1;
# decode MediaWiki page section name
$anchor =~ tr/./%/;
$anchor = url_decode($anchor);
# decode MediaWiki page section name (only correct UTF8 sequences)
$anchor =~ s/((?:
\.[0-7][A-F0-9]|
\.[CD][A-F0-9]\.[89AB][A-F0-9]|
\.E[A-F0-9](?:\.[89AB][A-F0-9]){2}|
\.F[0-7](?:\.[89AB][A-F0-9]){3}
)+)/($a = $1), ($a =~ tr!.!\%!), (url_decode($a))/gesx;
$anchor =~ tr/_/ /;
}
$article =~ s/&.*$//so if $wikiurl =~ /title=$/so;
@ -434,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>";
@ -948,6 +956,21 @@ sub create
# html_quote in the form of a function
html => \&html_quote,
# escape regular expression characters
regex_escape => sub
{
my ($s) = @_;
return "\Q$s\E";
},
# escape regular expression replacement characters
replacement_escape => sub
{
my ($s) = @_;
$s =~ s/([\\\$])/\\$1/gso;
return $s;
},
# HTML <select>
# html_select(name, <selected value>, <values>, [<value names>], [<attr_hash>])
# <values> may be one of:

View File

@ -653,17 +653,16 @@ sub get_products_by_permission
sub get_editable_products
{
my ($self, $classification_id) = @_;
my $sql = "SELECT DISTINCT products.id FROM products";
my $t = "WHERE";
my $sql = "SELECT DISTINCT p.id FROM products p";
if (!$self->in_group('editcomponents'))
{
$sql .= ", group_control_map WHERE editcomponents=1 AND group_id IN (".$self->groups_as_string.")";
$t = "AND";
$sql .= " INNER JOIN group_control_map gm ON gm.product_id=p.id".
" AND gm.editcomponents=1 AND gm.group_id IN (".$self->groups_as_string.")";
}
if ($classification_id)
{
# restrict product list by classification
$sql .= " $t classification_id=".int($classification_id);
$sql .= " WHERE classification_id=".int($classification_id);
}
return Bugzilla::Product->new_from_list(Bugzilla->dbh->selectcol_arrayref($sql) || []);
}
@ -971,7 +970,7 @@ sub check_can_admin_product
# First make sure the product name is valid.
my $product = Bugzilla::Product::check_product($product_name);
($self->in_group('editcomponents', $product->id) && $self->can_see_product($product->name))
$self->in_group('editcomponents', $product->id)
|| $self->in_group('editcomponents')
|| ThrowUserError('product_admin_denied', {product => $product->name});

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)
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);
@ -932,7 +932,7 @@ sub xml_dump_simple
}
elsif ($data =~ 'HASH')
{
$r = join '', map { xml_element($_, '', xml_dump_simple($data->{$_})) } keys %$data;
$r = join '', map { xml_element((/^[a-z:_][a-z:_\.0-9]*$/is ? ($_, '') : ('i', { key => $_ })), xml_dump_simple($data->{$_})) } keys %$data;
}
elsif ($data =~ 'SCALAR')
{

View File

@ -72,8 +72,6 @@ sub refresh_some_views
my ($userid) = $dbh->selectrow_array('SELECT userid FROM profiles WHERE login_name LIKE ? ORDER BY userid LIMIT 1', undef, $q.'@%');
$userid or next;
my $userobj = Bugzilla::User->new($userid) or next;
# Modify current user (hack)
Bugzilla->request_cache->{user} = $userobj;
# Determine saved search
$q = $query;
$q =~ tr/_/%/;
@ -82,11 +80,11 @@ sub refresh_some_views
my $storedquery = Bugzilla::Search::Saved->new({ name => $q, user => $userid }) or next;
$storedquery = http_decode_query($storedquery->query);
# get SQL code
my $search = new Bugzilla::Search(
my $search = eval { new Bugzilla::Search(
params => $storedquery,
fields => [ 'bug_id', grep { $_ ne 'bug_id' } split(/[ ,]+/, $storedquery->{columnlist} || '') ],
user => $userobj,
) or next;
) } or next;
# Re-create views
my $drop = "DROP VIEW IF EXISTS view\$$user\$$query\$";
my $create = "CREATE VIEW view\$$user\$$query\$";
@ -108,11 +106,9 @@ sub refresh_some_views
$dbh->do($drop.'longdescs');
$dbh->do($drop.'bugs_activity');
$dbh->do($create.'bugs AS '.$sql);
$dbh->do($create.'longdescs AS SELECT l.bug_id, u.login_name, l.bug_when, l.thetext, l.work_time FROM longdescs l INNER JOIN '.$bugids.' b ON b.bug_id=l.bug_id INNER JOIN profiles u ON u.userid=l.who'.($userobj->is_insider?'':' WHERE l.isprivate=0'));
$dbh->do($create.'bugs_activity AS SELECT a.bug_id, u.login_name, a.bug_when, f.name field_name, a.removed, a.added FROM bugs_activity a INNER JOIN '.$bugids.' b ON b.bug_id=a.bug_id INNER JOIN profiles u ON u.userid=a.who INNER JOIN fielddefs f ON f.id=a.fieldid');
$dbh->do($create.'longdescs AS SELECT l.comment_id, l.bug_id, u.login_name, l.bug_when, l.thetext, l.work_time FROM longdescs l INNER JOIN '.$bugids.' b ON b.bug_id=l.bug_id INNER JOIN profiles u ON u.userid=l.who'.($userobj->is_insider?'':' WHERE l.isprivate=0'));
$dbh->do($create.'bugs_activity AS SELECT a.id, a.bug_id, u.login_name, a.bug_when, f.name field_name, a.removed, a.added FROM bugs_activity a INNER JOIN '.$bugids.' b ON b.bug_id=a.bug_id INNER JOIN profiles u ON u.userid=a.who INNER JOIN fielddefs f ON f.id=a.fieldid');
}
# Restore current user
Bugzilla->request_cache->{user} = $old_user;
}
1;

View File

@ -45,6 +45,12 @@ sub type {
if ($type eq 'dateTime') {
$value = $self->datetime_format_outbound($value);
}
elsif ($type eq 'email') {
$type = 'string';
if (Bugzilla->params->{'webservice_email_filter'}) {
$value = email_filter($value);
}
}
return XMLRPC::Data->type($type)->value($value);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,90 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Mads Bondo Dydensborg <mbd@dbc.dk>
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Bugzilla;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Constants;
use Bugzilla::Util qw(datetime_from);
use Bugzilla::WebService::Util qw(validate filter_wants);
use Bugzilla::Util qw(trick_taint);
use DateTime;
# Basic info that is needed before logins
use constant LOGIN_EXEMPT => {
parameters => 1,
timezone => 1,
version => 1,
};
use constant READ_ONLY => qw(
extensions
parameters
timezone
time
version
);
use constant PUBLIC_METHODS => qw(
extensions
last_audit_time
parameters
time
timezone
version
);
# Logged-out users do not need to know more than that.
use constant PARAMETERS_LOGGED_OUT => qw(
maintainer
requirelogin
);
# These parameters are guessable from the web UI when the user
# is logged in. So it's safe to access them.
use constant PARAMETERS_LOGGED_IN => qw(
allowemailchange
attachment_base
commentonchange_resolution
commentonduplicate
cookiepath
defaultopsys
defaultplatform
defaultpriority
defaultseverity
duplicate_or_move_bug_status
emailregexpdesc
emailsuffix
letsubmitterchoosemilestone
letsubmitterchoosepriority
mailfrom
maintainer
maxattachmentsize
maxlocalattachment
musthavemilestoneonaccept
noresolveonopenblockers
password_complexity
rememberlogin
requirelogin
search_allow_no_criteria
urlbase
use_see_also
useclassification
usemenuforusers
useqacontact
usestatuswhiteboard
usetargetmilestone
);
sub version {
my $self = shift;
return { version => $self->type('string', BUGZILLA_VERSION) };
@ -74,6 +121,32 @@ sub time {
};
}
sub last_audit_time {
my ($self, $params) = validate(@_, 'class');
return {
last_audit_time => undef
};
}
sub parameters {
my ($self, $args) = @_;
my $user = Bugzilla->login(LOGIN_OPTIONAL);
my $params = Bugzilla->params;
$args ||= {};
my @params_list = $user->in_group('tweakparams')
? keys(%$params)
: $user->id ? PARAMETERS_LOGGED_IN : PARAMETERS_LOGGED_OUT;
my %parameters;
foreach my $param (@params_list) {
next unless filter_wants($args, $param);
$parameters{$param} = $self->type('string', $params->{$param});
}
return { parameters => \%parameters };
}
1;
__END__
@ -91,9 +164,11 @@ This provides functions that tell you about Bugzilla in general.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=over
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
the directions for how to access the data via REST is noted in each method
where applicable.
=item C<version>
=head2 version
B<STABLE>
@ -103,6 +178,12 @@ B<STABLE>
Returns the current version of Bugzilla.
=item B<REST>
GET /rest/version
The returned data format is the same as below.
=item B<Params> (none)
=item B<Returns>
@ -112,9 +193,17 @@ string.
=item B<Errors> (none)
=item B<History>
=over
=item REST API call added in Bugzilla B<5.0>.
=back
=item C<extensions>
=back
=head2 extensions
B<EXPERIMENTAL>
@ -125,6 +214,12 @@ B<EXPERIMENTAL>
Gets information about the extensions that are currently installed and enabled
in this Bugzilla.
=item B<REST>
GET /rest/extensions
The returned data format is the same as below.
=item B<Params> (none)
=item B<Returns>
@ -155,11 +250,13 @@ The return value looks something like this:
that the extensions define themselves. Before 3.6, the names of the
extensions depended on the directory they were in on the Bugzilla server.
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=item C<timezone>
=head2 timezone
B<DEPRECATED> This method may be removed in a future version of Bugzilla.
Use L</time> instead.
@ -170,6 +267,12 @@ Use L</time> instead.
Returns the timezone that Bugzilla expects dates and times in.
=item B<REST>
GET /rest/timezone
The returned data format is the same as below.
=item B<Params> (none)
=item B<Returns>
@ -184,12 +287,14 @@ string in (+/-)XXXX (RFC 2822) format.
=item As of Bugzilla B<3.6>, the timezone returned is always C<+0000>
(the UTC timezone).
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=item C<time>
=head2 time
B<STABLE>
@ -200,6 +305,12 @@ B<STABLE>
Gets information about what time the Bugzilla server thinks it is, and
what timezone it's running in.
=item B<REST>
GET /rest/time
The returned data format is the same as below.
=item B<Params> (none)
=item B<Returns>
@ -210,7 +321,7 @@ A struct with the following items:
=item C<db_time>
C<dateTime> The current time in UTC, according to the Bugzilla
C<dateTime> The current time in UTC, according to the Bugzilla
I<database server>.
Note that Bugzilla assumes that the database and the webserver are running
@ -220,7 +331,7 @@ rely on for doing searches and other input to the WebService.
=item C<web_time>
C<dateTime> This is the current time in UTC, according to Bugzilla's
C<dateTime> This is the current time in UTC, according to Bugzilla's
I<web server>.
This might be different by a second from C<db_time> since this comes from
@ -236,7 +347,7 @@ versions of Bugzilla before 3.6.)
=item C<tz_name>
C<string> The literal string C<UTC>. (Exists only for backwards-compatibility
with versions of Bugzilla before 3.6.)
with versions of Bugzilla before 3.6.)
=item C<tz_short_name>
@ -260,9 +371,136 @@ with versions of Bugzilla before 3.6.)
were in the UTC timezone, instead of returning information in the server's
local timezone.
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head2 parameters
B<UNSTABLE>
=over
=item B<Description>
Returns parameter values currently used in this Bugzilla.
=item B<REST>
GET /rest/parameters
The returned data format is the same as below.
=item B<Params> (none)
=item B<Returns>
A hash with a single item C<parameters> which contains a hash with
the name of the parameters as keys and their value as values. All
values are returned as strings.
The list of parameters returned by this method depends on the user
credentials:
A logged-out user can only access the C<maintainer> and C<requirelogin> parameters.
A logged-in user can access the following parameters (listed alphabetically):
C<allowemailchange>,
C<attachment_base>,
C<commentonchange_resolution>,
C<commentonduplicate>,
C<cookiepath>,
C<defaultopsys>,
C<defaultplatform>,
C<defaultpriority>,
C<defaultseverity>,
C<duplicate_or_move_bug_status>,
C<emailregexpdesc>,
C<emailsuffix>,
C<letsubmitterchoosemilestone>,
C<letsubmitterchoosepriority>,
C<mailfrom>,
C<maintainer>,
C<maxattachmentsize>,
C<maxlocalattachment>,
C<musthavemilestoneonaccept>,
C<noresolveonopenblockers>,
C<password_complexity>,
C<rememberlogin>,
C<requirelogin>,
C<search_allow_no_criteria>,
C<urlbase>,
C<use_see_also>,
C<useclassification>,
C<usemenuforusers>,
C<useqacontact>,
C<usestatuswhiteboard>,
C<usetargetmilestone>.
A user in the tweakparams group can access all existing parameters.
New parameters can appear or obsolete parameters can disappear depending
on the version of Bugzilla and on extensions being installed.
The list of parameters returned by this method is not stable and will
never be stable.
=item B<History>
=over
=item Added in Bugzilla B<4.4>.
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head2 last_audit_time
B<EXPERIMENTAL>
=over
=item B<Description>
Gets the latest time of the audit_log table.
=item B<REST>
GET /rest/last_audit_time
The returned data format is the same as below.
=item B<Params>
You can pass the optional parameter C<class> to get the maximum for only
the listed classes.
=over
=item C<class> (array) - An array of strings representing the class names.
B<Note:> The class names are defined as "Bugzilla::<class_name>". For the product
use Bugzilla:Product.
=back
=item B<Returns>
A hash with a single item, C<last_audit_time>, that is the maximum of the
at_time from the audit_log.
=item B<Errors> (none)
=item B<History>
=over
=item Added in Bugzilla B<4.4>.
=item REST API call added in Bugzilla B<5.0>.
=back
=back

View File

@ -0,0 +1,212 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Classification;
use strict;
use warnings;
use base qw (Bugzilla::WebService);
use Bugzilla::Classification;
use Bugzilla::Error;
use Bugzilla::WebService::Util qw(filter validate params_to_objects);
use constant READ_ONLY => qw(
get
);
use constant PUBLIC_METHODS => qw(
get
);
sub get {
my ($self, $params) = validate(@_, 'names', 'ids');
defined $params->{names} || defined $params->{ids}
|| ThrowCodeError('params_required', { function => 'Classification.get',
params => ['names', 'ids'] });
my $user = Bugzilla->user;
Bugzilla->params->{'useclassification'}
|| $user->in_group('editclassifications')
|| ThrowUserError('auth_classification_not_enabled');
Bugzilla->switch_to_shadow_db;
my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') };
unless ($user->in_group('editclassifications')) {
my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications};
@classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
}
my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
return { classifications => \@classifications };
}
sub _classification_to_hash {
my ($self, $classification, $params) = @_;
my $user = Bugzilla->user;
return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
my $products = $user->in_group('editclassifications') ?
$classification->products : $user->get_selectable_products($classification->id);
return filter $params, {
id => $self->type('int', $classification->id),
name => $self->type('string', $classification->name),
description => $self->type('string', $classification->description),
sort_key => $self->type('int', $classification->sortkey),
products => [ map { $self->_product_to_hash($_, $params) } @$products ],
};
}
sub _product_to_hash {
my ($self, $product, $params) = @_;
return filter $params, {
id => $self->type('int', $product->id),
name => $self->type('string', $product->name),
description => $self->type('string', $product->description),
}, undef, 'products';
}
1;
__END__
=head1 NAME
Bugzilla::Webservice::Classification - The Classification API
=head1 DESCRIPTION
This part of the Bugzilla API allows you to deal with the available Classifications.
You will be able to get information about them as well as manipulate them.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
the directions for how to access the data via REST is noted in each method
where applicable.
=head1 Classification Retrieval
=head2 get
B<EXPERIMENTAL>
=over
=item B<Description>
Returns a hash containing information about a set of classifications.
=item B<REST>
To return information on a single classification:
GET /rest/classification/<classification_id_or_name>
The returned data format will be the same as below.
=item B<Params>
In addition to the parameters below, this method also accepts the
standard L<include_fields|Bugzilla::WebService/include_fields> and
L<exclude_fields|Bugzilla::WebService/exclude_fields> arguments.
You could get classifications info by supplying their names and/or ids.
So, this method accepts the following parameters:
=over
=item C<ids>
An array of classification ids.
=item C<names>
An array of classification names.
=back
=item B<Returns>
A hash with the key C<classifications> and an array of hashes as the corresponding value.
Each element of the array represents a classification that the user is authorized to see
and has the following keys:
=over
=item C<id>
C<int> The id of the classification.
=item C<name>
C<string> The name of the classification.
=item C<description>
C<string> The description of the classificaion.
=item C<sort_key>
C<int> The value which determines the order the classification is sorted.
=item C<products>
An array of hashes. The array contains the products the user is authorized to
access within the classification. Each hash has the following keys:
=over
=item C<name>
C<string> The name of the product.
=item C<id>
C<int> The id of the product.
=item C<description>
C<string> The description of the product.
=back
=back
=item B<Errors>
=over
=item 900 (Classification not enabled)
Classification is not enabled on this installation.
=back
=item B<History>
=over
=item Added in Bugzilla B<4.4>.
=item REST API call added in Bugzilla B<5.0>.
=back
=back

View File

@ -0,0 +1,153 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Component;
use 5.10.1;
use strict;
use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Component;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(translate params_to_objects validate);
use constant PUBLIC_METHODS => qw(
create
);
use constant MAPPED_FIELDS => {
default_assignee => 'initialowner',
default_qa_contact => 'initialqacontact',
default_cc => 'initial_cc',
is_open => 'isactive',
};
sub create {
my ($self, $params) = @_;
my $user = Bugzilla->login(LOGIN_REQUIRED);
$user->in_group('editcomponents')
|| scalar @{ $user->get_products_by_permission('editcomponents') }
|| ThrowUserError('auth_failure', { group => 'editcomponents',
action => 'edit',
object => 'components' });
my $product = $user->check_can_admin_product($params->{product});
# Translate the fields
my $values = translate($params, MAPPED_FIELDS);
$values->{product} = $product;
# Create the component and return the newly created id.
my $component = Bugzilla::Component->create($values);
return { id => $self->type('int', $component->id) };
}
1;
__END__
=head1 NAME
Bugzilla::Webservice::Component - The Component API
=head1 DESCRIPTION
This part of the Bugzilla API allows you to deal with the available product components.
You will be able to get information about them as well as manipulate them.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=head1 Component Creation and Modification
=head2 create
B<EXPERIMENTAL>
=over
=item B<Description>
This allows you to create a new component in Bugzilla.
=item B<Params>
Some params must be set, or an error will be thrown. These params are
marked B<Required>.
=over
=item C<name>
B<Required> C<string> The name of the new component.
=item C<product>
B<Required> C<string> The name of the product that the component must be
added to. This product must already exist, and the user have the necessary
permissions to edit components for it.
=item C<description>
B<Required> C<string> The description of the new component.
=item C<default_assignee>
B<Required> C<string> The login name of the default assignee of the component.
=item C<default_cc>
C<array> An array of strings with each element representing one login name of the default CC list.
=item C<default_qa_contact>
C<string> The login name of the default QA contact for the component.
=item C<is_open>
C<boolean> 1 if you want to enable the component for bug creations. 0 otherwise. Default is 1.
=back
=item B<Returns>
A hash with one key: C<id>. This will represent the ID of the newly-added
component.
=item B<Errors>
=over
=item 304 (Authorization Failure)
You are not authorized to create a new component.
=item 1200 (Component already exists)
The name that you specified for the new component already exists in the
specified product.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back

View File

@ -1,30 +1,37 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Constants;
use 5.10.1;
use strict;
use warnings;
use base qw(Exporter);
our @EXPORT = qw(
WS_ERROR_CODE
STATUS_OK
STATUS_CREATED
STATUS_ACCEPTED
STATUS_NO_CONTENT
STATUS_MULTIPLE_CHOICES
STATUS_BAD_REQUEST
STATUS_NOT_FOUND
STATUS_GONE
REST_STATUS_CODE_MAP
ERROR_UNKNOWN_FATAL
ERROR_UNKNOWN_TRANSIENT
XMLRPC_CONTENT_TYPE_WHITELIST
REST_CONTENT_TYPE_WHITELIST
WS_DISPATCH
);
@ -49,13 +56,17 @@ our @EXPORT = qw(
use constant WS_ERROR_CODE => {
# Generic errors (Bugzilla::Object and others) are 50-99.
object_not_specified => 50,
reassign_to_empty => 50,
param_required => 50,
params_required => 50,
undefined_field => 50,
object_does_not_exist => 51,
param_must_be_numeric => 52,
number_not_numeric => 52,
param_invalid => 53,
number_too_large => 54,
number_too_small => 55,
illegal_date => 56,
# Bug errors usually occupy the 100-200 range.
improper_bug_id_field_value => 100,
bug_id_does_not_exist => 101,
@ -66,12 +77,14 @@ use constant WS_ERROR_CODE => {
alias_in_use => 103,
alias_is_numeric => 103,
alias_has_comma_or_space => 103,
multiple_alias_not_allowed => 103,
# Misc. bug field errors
illegal_field => 104,
freetext_too_long => 104,
# Component errors
require_component => 105,
component_name_too_long => 105,
require_component => 105,
component_name_too_long => 105,
product_unknown_component => 105,
# Invalid Product
no_products => 106,
entry_access_denied => 106,
@ -87,6 +100,12 @@ use constant WS_ERROR_CODE => {
comment_is_private => 110,
comment_id_invalid => 111,
comment_too_long => 114,
comment_invalid_isprivate => 117,
# Comment tagging
comment_tag_disabled => 125,
comment_tag_invalid => 126,
comment_tag_too_long => 127,
comment_tag_too_short => 128,
# See Also errors
bug_url_invalid => 112,
bug_url_too_long => 112,
@ -95,14 +114,39 @@ use constant WS_ERROR_CODE => {
# Note: 114 is above in the Comment-related section.
# Bug update errors
illegal_change => 115,
# Dependency errors
dependency_loop_single => 116,
dependency_loop_multi => 116,
# Note: 117 is above in the Comment-related section.
# Dup errors
dupe_loop_detected => 118,
dupe_id_required => 119,
# Bug-related group errors
group_invalid_removal => 120,
group_restriction_not_allowed => 120,
# Status/Resolution errors
missing_resolution => 121,
resolution_not_allowed => 122,
illegal_bug_status_transition => 123,
# Flag errors
flag_status_invalid => 129,
flag_update_denied => 130,
flag_type_requestee_disabled => 131,
flag_not_unique => 132,
flag_type_not_unique => 133,
flag_type_inactive => 134,
# Authentication errors are usually 300-400.
invalid_username_or_password => 300,
invalid_login_or_password => 300,
account_disabled => 301,
auth_invalid_email => 302,
extern_id_conflict => -303,
auth_failure => 304,
password_current_too_short => 305,
password_too_short => 305,
password_not_complex => 305,
api_key_not_valid => 306,
api_key_revoked => 306,
auth_invalid_token => 307,
# Except, historically, AUTH_NODATA, which is 410.
login_required => 410,
@ -122,10 +166,105 @@ use constant WS_ERROR_CODE => {
user_access_by_id_denied => 505,
user_access_by_match_denied => 505,
# RPC Server Errors. See the following URL:
# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
xmlrpc_invalid_value => -32600,
unknown_method => -32601,
# Attachment errors are 600-700.
file_too_large => 600,
invalid_content_type => 601,
# Error 602 attachment_illegal_url no longer exists.
file_not_specified => 603,
missing_attachment_description => 604,
# Error 605 attachment_url_disabled no longer exists.
zero_length_file => 606,
# Product erros are 700-800
product_blank_name => 700,
product_name_too_long => 701,
product_name_already_in_use => 702,
product_name_diff_in_case => 702,
product_must_have_description => 703,
product_must_have_version => 704,
product_must_define_defaultmilestone => 705,
# Group errors are 800-900
empty_group_name => 800,
group_exists => 801,
empty_group_description => 802,
invalid_regexp => 803,
invalid_group_name => 804,
group_cannot_view => 805,
# Classification errors are 900-1000
auth_classification_not_enabled => 900,
# Search errors are 1000-1100
buglist_parameters_required => 1000,
# Flag type errors are 1100-1200
flag_type_name_invalid => 1101,
flag_type_description_invalid => 1102,
flag_type_cc_list_invalid => 1103,
flag_type_sortkey_invalid => 1104,
flag_type_not_editable => 1105,
# Component errors are 1200-1300
component_already_exists => 1200,
component_is_last => 1201,
component_has_bugs => 1202,
# Errors thrown by the WebService itself. The ones that are negative
# conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
xmlrpc_invalid_value => -32600,
unknown_method => -32601,
json_rpc_post_only => 32610,
json_rpc_invalid_callback => 32611,
xmlrpc_illegal_content_type => 32612,
json_rpc_illegal_content_type => 32613,
rest_invalid_resource => 32614,
};
# RESTful webservices use the http status code
# to describe whether a call was successful or
# to describe the type of error that occurred.
use constant STATUS_OK => 200;
use constant STATUS_CREATED => 201;
use constant STATUS_ACCEPTED => 202;
use constant STATUS_NO_CONTENT => 204;
use constant STATUS_MULTIPLE_CHOICES => 300;
use constant STATUS_BAD_REQUEST => 400;
use constant STATUS_NOT_AUTHORIZED => 401;
use constant STATUS_NOT_FOUND => 404;
use constant STATUS_GONE => 410;
# The integer value is the error code above returned by
# the related webvservice call. We choose the appropriate
# http status code based on the error code or use the
# default STATUS_BAD_REQUEST.
sub REST_STATUS_CODE_MAP {
my $status_code_map = {
51 => STATUS_NOT_FOUND,
101 => STATUS_NOT_FOUND,
102 => STATUS_NOT_AUTHORIZED,
106 => STATUS_NOT_AUTHORIZED,
109 => STATUS_NOT_AUTHORIZED,
110 => STATUS_NOT_AUTHORIZED,
113 => STATUS_NOT_AUTHORIZED,
115 => STATUS_NOT_AUTHORIZED,
120 => STATUS_NOT_AUTHORIZED,
300 => STATUS_NOT_AUTHORIZED,
301 => STATUS_NOT_AUTHORIZED,
302 => STATUS_NOT_AUTHORIZED,
303 => STATUS_NOT_AUTHORIZED,
304 => STATUS_NOT_AUTHORIZED,
410 => STATUS_NOT_AUTHORIZED,
504 => STATUS_NOT_AUTHORIZED,
505 => STATUS_NOT_AUTHORIZED,
32614 => STATUS_NOT_FOUND,
_default => STATUS_BAD_REQUEST
};
Bugzilla::Hook::process('webservice_status_code_map',
{ status_code_map => $status_code_map });
return $status_code_map;
};
# These are the fallback defaults for errors not in ERROR_CODE.
@ -134,6 +273,19 @@ use constant ERROR_UNKNOWN_TRANSIENT => 32000;
use constant ERROR_GENERAL => 999;
use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
text/xml
application/xml
);
# The first content type specified is used as the default.
use constant REST_CONTENT_TYPE_WHITELIST => qw(
application/json
application/javascript
text/javascript
text/html
);
sub WS_DISPATCH {
# We "require" here instead of "use" above to avoid a dependency loop.
require Bugzilla::Hook;
@ -141,14 +293,28 @@ sub WS_DISPATCH {
Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
my $dispatch = {
'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
'Bug' => 'Bugzilla::WebService::Bug',
'User' => 'Bugzilla::WebService::User',
'Product' => 'Bugzilla::WebService::Product',
'Field' => 'Bugzilla::WebService::Field',
'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
'Bug' => 'Bugzilla::WebService::Bug',
'Classification' => 'Bugzilla::WebService::Classification',
'Component' => 'Bugzilla::WebService::Component',
'FlagType' => 'Bugzilla::WebService::FlagType',
'Group' => 'Bugzilla::WebService::Group',
'Product' => 'Bugzilla::WebService::Product',
'User' => 'Bugzilla::WebService::User',
'Field' => 'Bugzilla::WebService::Field',
%hook_dispatch
};
return $dispatch;
};
1;
=head1 B<Methods in need of POD>
=over
=item REST_STATUS_CODE_MAP
=item WS_DISPATCH
=back

View File

@ -0,0 +1,833 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::FlagType;
use strict;
use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Component;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::FlagType;
use Bugzilla::Product;
use Bugzilla::Util qw(trim);
use List::MoreUtils qw(uniq);
use constant PUBLIC_METHODS => qw(
create
get
update
);
sub get {
my ($self, $params) = @_;
my $dbh = Bugzilla->switch_to_shadow_db();
my $user = Bugzilla->user;
defined $params->{product}
|| ThrowCodeError('param_required',
{ function => 'Bug.flag_types',
param => 'product' });
my $product = delete $params->{product};
my $component = delete $params->{component};
$product = Bugzilla::Product->check({ name => $product, cache => 1 });
$component = Bugzilla::Component->check(
{ name => $component, product => $product, cache => 1 }) if $component;
my $flag_params = { product_id => $product->id };
$flag_params->{component_id} = $component->id if $component;
my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
my $flag_types = { bug => [], attachment => [] };
foreach my $flag_type (@$matched_flag_types) {
push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product))
if $flag_type->target_type eq 'bug';
push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product))
if $flag_type->target_type eq 'attachment';
}
return $flag_types;
}
sub create {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
Bugzilla->user->in_group('editcomponents')
|| scalar(@{$user->get_products_by_permission('editcomponents')})
|| ThrowUserError("auth_failure", { group => "editcomponents",
action => "add",
object => "flagtypes" });
$params->{name} || ThrowCodeError('param_required', { param => 'name' });
$params->{description} || ThrowCodeError('param_required', { param => 'description' });
my %args = (
sortkey => 1,
name => undef,
inclusions => ['0:0'], # Default to __ALL__:__ALL__
cc_list => '',
description => undef,
is_requestable => 'on',
exclusions => [],
is_multiplicable => 'on',
request_group => '',
is_active => 'on',
is_specifically_requestable => 'on',
target_type => 'bug',
grant_group => '',
);
foreach my $key (keys %args) {
$args{$key} = $params->{$key} if defined($params->{$key});
}
$args{name} = trim($params->{name});
$args{description} = trim($params->{description});
# Is specifically requestable is actually is_requesteeable
if (exists $args{is_specifically_requestable}) {
$args{is_requesteeble} = delete $args{is_specifically_requestable};
}
# Default is on for the tickbox flags.
# If the user has set them to 'off' then undefine them so the flags are not ticked
foreach my $arg_name (qw(is_requestable is_multiplicable is_active is_requesteeble)) {
if (defined($args{$arg_name}) && ($args{$arg_name} eq '0')) {
$args{$arg_name} = undef;
}
}
# Process group inclusions and exclusions
$args{inclusions} = _process_lists($params->{inclusions}) if defined $params->{inclusions};
$args{exclusions} = _process_lists($params->{exclusions}) if defined $params->{exclusions};
my $flagtype = Bugzilla::FlagType->create(\%args);
return { id => $self->type('int', $flagtype->id) };
}
sub update {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
Bugzilla->login(LOGIN_REQUIRED);
$user->in_group('editcomponents')
|| scalar(@{$user->get_products_by_permission('editcomponents')})
|| ThrowUserError("auth_failure", { group => "editcomponents",
action => "edit",
object => "flagtypes" });
defined($params->{names}) || defined($params->{ids})
|| ThrowCodeError('params_required',
{ function => 'FlagType.update', params => ['ids', 'names'] });
# Get the list of unique flag type ids we are updating
my @flag_type_ids = defined($params->{ids}) ? @{$params->{ids}} : ();
if (defined $params->{names}) {
push @flag_type_ids, map { $_->id }
@{ Bugzilla::FlagType::match({ name => $params->{names} }) };
}
@flag_type_ids = uniq @flag_type_ids;
# We delete names and ids to keep only new values to set.
delete $params->{names};
delete $params->{ids};
# Process group inclusions and exclusions
# We removed them from $params because these are handled differently
my $inclusions = _process_lists(delete $params->{inclusions}) if defined $params->{inclusions};
my $exclusions = _process_lists(delete $params->{exclusions}) if defined $params->{exclusions};
$dbh->bz_start_transaction();
my %changes = ();
foreach my $flag_type_id (@flag_type_ids) {
my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_type_id);
if ($can_fully_edit) {
$flagtype->set_all($params);
}
elsif (scalar keys %$params) {
ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
}
# Process the clusions
foreach my $type ('inclusions', 'exclusions') {
my $clusions = $type eq 'inclusions' ? $inclusions : $exclusions;
next if not defined $clusions;
my @extra_clusions = ();
if (!$user->in_group('editcomponents')) {
my $products = $user->get_products_by_permission('editcomponents');
# Bring back the products the user cannot edit.
foreach my $item (values %{$flagtype->$type}) {
my ($prod_id, $comp_id) = split(':', $item);
push(@extra_clusions, $item) unless grep { $_->id == $prod_id } @$products;
}
}
$flagtype->set_clusions({
$type => [@$clusions, @extra_clusions],
});
}
my $returned_changes = $flagtype->update();
$changes{$flagtype->id} = {
name => $flagtype->name,
changes => $returned_changes,
};
}
$dbh->bz_commit_transaction();
my @result;
foreach my $flag_type_id (keys %changes) {
my %hash = (
id => $self->type('int', $flag_type_id),
name => $self->type('string', $changes{$flag_type_id}{name}),
changes => {},
);
foreach my $field (keys %{ $changes{$flag_type_id}{changes} }) {
my $change = $changes{$flag_type_id}{changes}{$field};
$hash{changes}{$field} = {
removed => $self->type('string', $change->[0]),
added => $self->type('string', $change->[1])
};
}
push(@result, \%hash);
}
return { flagtypes => \@result };
}
sub _flagtype_to_hash {
my ($self, $flagtype, $product) = @_;
my $user = Bugzilla->user;
my @values = ('X');
push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
push(@values, '+', '-') if $user->can_set_flag($flagtype);
my $item = {
id => $self->type('int' , $flagtype->id),
name => $self->type('string' , $flagtype->name),
description => $self->type('string' , $flagtype->description),
type => $self->type('string' , $flagtype->target_type),
values => \@values,
is_active => $self->type('boolean', $flagtype->is_active),
is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble),
is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
};
if ($product) {
my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
# if we have both inclusions and exclusions, the exclusions are redundant
$exclusions = [] if @$inclusions && @$exclusions;
# no need to return anything if there's just "any component"
$item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
$item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
}
return $item;
}
sub _flagtype_clusions_to_hash {
my ($self, $clusions, $product_id) = @_;
my $result = [];
foreach my $key (keys %$clusions) {
my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
if ($prod_id == 0 || $prod_id == $product_id) {
if ($comp_id) {
push @$result, $comp_id;
}
else {
return [ '' ];
}
}
}
$result = Bugzilla::Component->match({ product_id => $product_id, name => $result });
return $result;
}
sub _process_lists {
my $list = shift;
my $user = Bugzilla->user;
my @products;
if ($user->in_group('editcomponents')) {
@products = Bugzilla::Product->get_all;
}
else {
@products = @{$user->get_products_by_permission('editcomponents')};
}
my @component_list;
foreach my $item (@$list) {
# A hash with products as the key and component names as the values
if(ref($item) eq 'HASH') {
while (my ($product_name, $component_names) = each %$item) {
my $product = Bugzilla::Product->check({name => $product_name});
unless (grep { $product->name eq $_->name } @products) {
ThrowUserError('product_access_denied', { name => $product_name });
}
my @component_ids;
foreach my $comp_name (@$component_names) {
my $component = Bugzilla::Component->check({product => $product, name => $comp_name});
ThrowCodeError('param_invalid', { param => $comp_name}) unless defined $component;
push @component_list, $product->id . ':' . $component->id;
}
}
}
elsif(!ref($item)) {
# These are whole products
my $product = Bugzilla::Product->check({name => $item});
unless (grep { $product->name eq $_->name } @products) {
ThrowUserError('product_access_denied', { name => $item });
}
push @component_list, $product->id . ':0';
}
else {
# The user has passed something invalid
ThrowCodeError('param_invalid', { param => $item });
}
}
return \@component_list;
}
1;
__END__
=head1 NAME
Bugzilla::WebService::FlagType - API for creating flags.
=head1 DESCRIPTION
This part of the Bugzilla API allows you to create new flags
=head1 METHODS
See L<Bugzilla::WebService> for a description of what B<STABLE>, B<UNSTABLE>,
and B<EXPERIMENTAL> mean, and for more description about error codes.
=head2 Get Flag Types
=over
=item C<get> B<UNSTABLE>
=item B<Description>
Get information about valid flag types that can be set for bugs and attachments.
=item B<REST>
You have several options for retreiving information about flag types. The first
part is the request method and the rest is the related path needed.
To get information about all flag types for a product:
GET /rest/flag_type/<product>
To get information about flag_types for a product and component:
GET /rest/flag_type/<product>/<component>
The returned data format is the same as below.
=item B<Params>
You must pass a product name and an optional component name.
=over
=item C<product> (string) - The name of a valid product.
=item C<component> (string) - An optional valid component name associated with the product.
=back
=item B<Returns>
A hash containing two keys, C<bug> and C<attachment>. Each key value is an array of hashes,
containing the following keys:
=over
=item C<id>
C<int> An integer id uniquely identifying this flag type.
=item C<name>
C<string> The name for the flag type.
=item C<type>
C<string> The target of the flag type which is either C<bug> or C<attachment>.
=item C<description>
C<string> The description of the flag type.
=item C<values>
C<array> An array of string values that the user can set on the flag type.
=item C<is_requesteeble>
C<boolean> Users can ask specific other users to set flags of this type.
=item C<is_multiplicable>
C<boolean> Multiple flags of this type can be set for the same bug or attachment.
=back
=item B<Errors>
=over
=item 106 (Product Access Denied)
Either the product does not exist or you don't have access to it.
=item 51 (Invalid Component)
The component provided does not exist in the product.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back
=head2 Create Flag
=over
=item C<create> B<UNSTABLE>
=item B<Description>
Creates a new FlagType
=item B<REST>
POST /rest/flag_type
The params to include in the POST body as well as the returned data format,
are the same as below.
=item B<Params>
At a minimum the following two arguments must be supplied:
=over
=item C<name> (string) - The name of the new Flag Type.
=item C<description> (string) - A description for the Flag Type object.
=back
=item B<Returns>
C<int> flag_id
The ID of the new FlagType object is returned.
=item B<Params>
=over
=item name B<required>
C<string> A short name identifying this type.
=item description B<required>
C<string> A comprehensive description of this type.
=item inclusions B<optional>
An array of strings or a hash containing product names, and optionally
component names. If you provide a string, the flag type will be shown on
all bugs in that product. If you provide a hash, the key represents the
product name, and the value is the components of the product to be included.
For example:
[ 'FooProduct',
{
BarProduct => [ 'C1', 'C3' ],
BazProduct => [ 'C7' ]
}
]
This flag will be added to B<All> components of I<FooProduct>,
components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
=item exclusions B<optional>
An array of strings or hashes containing product names. This uses the same
fromat as inclusions.
This will exclude the flag from all products and components specified.
=item sortkey B<optional>
C<int> A number between 1 and 32767 by which this type will be sorted when
displayed to users in a list; ignore if you don't care what order the types
appear in or if you want them to appear in alphabetical order.
=item is_active B<optional>
C<boolean> Flag of this type appear in the UI and can be set. Default is B<true>.
=item is_requestable B<optional>
C<boolean> Users can ask for flags of this type to be set. Default is B<true>.
=item cc_list B<optional>
C<array> An array of strings. If the flag type is requestable, who should
receive e-mail notification of requests. This is an array of e-mail addresses
which do not need to be Bugzilla logins.
=item is_specifically_requestable B<optional>
C<boolean> Users can ask specific other users to set flags of this type as
opposed to just asking the wind. Default is B<true>.
=item is_multiplicable B<optional>
C<boolean> Multiple flags of this type can be set on the same bug. Default is B<true>.
=item grant_group B<optional>
C<string> The group allowed to grant/deny flags of this type (to allow all
users to grant/deny these flags, select no group). Default is B<no group>.
=item request_group B<optional>
C<string> If flags of this type are requestable, the group allowed to request
them (to allow all users to request these flags, select no group). Note that
the request group alone has no effect if the grant group is not defined!
Default is B<no group>.
=back
=item B<Errors>
=over
=item 51 (Group Does Not Exist)
The group name you entered does not exist, or you do not have access to it.
=item 105 (Unknown component)
The component does not exist for this product.
=item 106 (Product Access Denied)
Either the product does not exist or you don't have editcomponents privileges
to it.
=item 501 (Illegal Email Address)
One of the e-mail address in the CC list is invalid. An e-mail in the CC
list does NOT need to be a valid Bugzilla user.
=item 1101 (Flag Type Name invalid)
You must specify a non-blank name for this flag type. It must
no contain spaces or commas, and must be 50 characters or less.
=item 1102 (Flag type must have description)
You must specify a description for this flag type.
=item 1103 (Flag type CC list is invalid
The CC list must be 200 characters or less.
=item 1104 (Flag Type Sort Key Not Valid)
The sort key is not a valid number.
=item 1105 (Flag Type Not Editable)
This flag type is not available for the products you can administer. Therefore
you can not edit attributes of the flag type, other than the inclusion and
exclusion list.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back
=head2 update
B<EXPERIMENTAL>
=over
=item B<Description>
This allows you to update a flag type in Bugzilla.
=item B<REST>
PUT /rest/flag_type/<product_id_or_name>
The params to include in the PUT body as well as the returned data format,
are the same as below. The C<ids> and C<names> params will be overridden as
it is pulled from the URL path.
=item B<Params>
B<Note:> The following parameters specify which products you are updating.
You must set one or both of these parameters.
=over
=item C<ids>
C<array> of C<int>s. Numeric ids of the flag types that you wish to update.
=item C<names>
C<array> of C<string>s. Names of the flag types that you wish to update. If
many flag types have the same name, this will change ALL of them.
=back
B<Note:> The following parameters specify the new values you want to set for
the products you are updating.
=over
=item name
C<string> A short name identifying this type.
=item description
C<string> A comprehensive description of this type.
=item inclusions B<optional>
An array of strings or a hash containing product names, and optionally
component names. If you provide a string, the flag type will be shown on
all bugs in that product. If you provide a hash, the key represents the
product name, and the value is the components of the product to be included.
for example
[ 'FooProduct',
{
BarProduct => [ 'C1', 'C3' ],
BazProduct => [ 'C7' ]
}
]
This flag will be added to B<All> components of I<FooProduct>,
components C1 and C3 of I<BarProduct>, and C7 of I<BazProduct>.
=item exclusions B<optional>
An array of strings or hashes containing product names.
This uses the same fromat as inclusions.
This will exclude the flag from all products and components specified.
=item sortkey
C<int> A number between 1 and 32767 by which this type will be sorted when
displayed to users in a list; ignore if you don't care what order the types
appear in or if you want them to appear in alphabetical order.
=item is_active
C<boolean> Flag of this type appear in the UI and can be set.
=item is_requestable
C<boolean> Users can ask for flags of this type to be set.
=item cc_list
C<array> An array of strings. If the flag type is requestable, who should
receive e-mail notification of requests. This is an array of e-mail addresses
which do not need to be Bugzilla logins.
=item is_specifically_requestable
C<boolean> Users can ask specific other users to set flags of this type as
opposed to just asking the wind.
=item is_multiplicable
C<boolean> Multiple flags of this type can be set on the same bug.
=item grant_group
C<string> The group allowed to grant/deny flags of this type (to allow all
users to grant/deny these flags, select no group).
=item request_group
C<string> If flags of this type are requestable, the group allowed to request
them (to allow all users to request these flags, select no group). Note that
the request group alone has no effect if the grant group is not defined!
=back
=item B<Returns>
A C<hash> with a single field "flagtypes". This points to an array of hashes
with the following fields:
=over
=item C<id>
C<int> The id of the product that was updated.
=item C<name>
C<string> The name of the product that was updated.
=item C<changes>
C<hash> The changes that were actually done on this product. The keys are
the names of the fields that were changed, and the values are a hash
with two keys:
=over
=item C<added>
C<string> The value that this field was changed to.
=item C<removed>
C<string> The value that was previously set in this field.
=back
Note that booleans will be represented with the strings '1' and '0'.
Here's an example of what a return value might look like:
{
products => [
{
id => 123,
changes => {
name => {
removed => 'FooFlagType',
added => 'BarFlagType'
},
is_requestable => {
removed => '1',
added => '0',
}
}
}
]
}
=back
=item B<Errors>
=over
=item 51 (Group Does Not Exist)
The group name you entered does not exist, or you do not have access to it.
=item 105 (Unknown component)
The component does not exist for this product.
=item 106 (Product Access Denied)
Either the product does not exist or you don't have editcomponents privileges
to it.
=item 501 (Illegal Email Address)
One of the e-mail address in the CC list is invalid. An e-mail in the CC
list does NOT need to be a valid Bugzilla user.
=item 1101 (Flag Type Name invalid)
You must specify a non-blank name for this flag type. It must
no contain spaces or commas, and must be 50 characters or less.
=item 1102 (Flag type must have description)
You must specify a description for this flag type.
=item 1103 (Flag type CC list is invalid
The CC list must be 200 characters or less.
=item 1104 (Flag Type Sort Key Not Valid)
The sort key is not a valid number.
=item 1105 (Flag Type Not Editable)
This flag type is not available for the products you can administer. Therefore
you can not edit attributes of the flag type, other than the inclusion and
exclusion list.
=back
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=back
=back

View File

@ -0,0 +1,695 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Group;
use strict;
use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::WebService::Util qw(validate translate params_to_objects user_to_hash);
use constant PUBLIC_METHODS => qw(
create
get
update
add_members
remove_members
add_managers
remove_managers
);
use constant MAPPED_RETURNS => {
userregexp => 'user_regexp',
isactive => 'is_active'
};
sub create {
my ($self, $params) = @_;
Bugzilla->login(LOGIN_REQUIRED);
Bugzilla->user->in_group('creategroups')
|| ThrowUserError("auth_failure", { group => "creategroups",
action => "add",
object => "group"});
# Create group
my $group = Bugzilla::Group->create({
name => $params->{name},
description => $params->{description},
userregexp => $params->{user_regexp},
isactive => $params->{is_active},
isbuggroup => 1,
icon_url => $params->{icon_url}
});
return { id => $self->type('int', $group->id) };
}
sub update {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
Bugzilla->login(LOGIN_REQUIRED);
Bugzilla->user->in_group('creategroups')
|| ThrowUserError("auth_failure", { group => "creategroups",
action => "edit",
object => "group" });
defined($params->{names}) || defined($params->{ids})
|| ThrowCodeError('params_required',
{ function => 'Group.update', params => ['ids', 'names'] });
my $group_objects = params_to_objects($params, 'Bugzilla::Group');
my %values = %$params;
# We delete names and ids to keep only new values to set.
delete $values{names};
delete $values{ids};
$dbh->bz_start_transaction();
foreach my $group (@$group_objects) {
$group->set_all(\%values);
}
my %changes;
foreach my $group (@$group_objects) {
my $returned_changes = $group->update();
$changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
}
$dbh->bz_commit_transaction();
my @result;
foreach my $group (@$group_objects) {
my %hash = (
id => $group->id,
changes => {},
);
foreach my $field (keys %{ $changes{$group->id} }) {
my $change = $changes{$group->id}->{$field};
$hash{changes}{$field} = {
removed => $self->type('string', $change->[0]),
added => $self->type('string', $change->[1])
};
}
push(@result, \%hash);
}
return { groups => \@result };
}
sub get {
my ($self, $params) = validate(@_, 'ids', 'names', 'type');
Bugzilla->login(LOGIN_REQUIRED);
# Reject access if there is no sense in continuing.
my $user = Bugzilla->user;
my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups');
if (!$all_groups && !$user->can_bless) {
ThrowUserError('group_cannot_view');
}
Bugzilla->switch_to_shadow_db();
my $groups = [];
if (defined $params->{ids}) {
# Get the groups by id
$groups = Bugzilla::Group->new_from_list($params->{ids});
}
if (defined $params->{names}) {
# Get the groups by name. Check will throw an error if a bad name is given
foreach my $name (@{$params->{names}}) {
# Skip if we got this from params->{id}
next if grep { $_->name eq $name } @$groups;
push @$groups, Bugzilla::Group->check({ name => $name });
}
}
if (!defined $params->{ids} && !defined $params->{names}) {
if ($all_groups) {
@$groups = Bugzilla::Group->get_all;
}
else {
# Get only groups the user has bless groups too
$groups = $user->bless_groups;
}
}
# Now create a result entry for each.
my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
return { groups => \@groups };
}
sub add_members
{
my $self = shift;
$self->_update_users(0, 1, @_);
}
sub remove_members
{
my $self = shift;
$self->_update_users(0, 0, @_);
}
sub add_managers
{
my $self = shift;
$self->_update_users(1, 1, @_);
}
sub remove_managers
{
my $self = shift;
$self->_update_users(1, 0, @_);
}
sub _update_users
{
my ($self, $isbless, $add, $params) = @_;
my $dbh = Bugzilla->dbh;
defined($params->{names}) || defined($params->{ids})
|| ThrowCodeError('params_required',
{ function => 'Group.update', params => ['ids', 'names'] });
my @group_objects;
if ($params->{names})
{
push @group_objects, @{ Bugzilla::Group->match({ name => $params->{names} }) };
}
if ($params->{ids})
{
push @group_objects, @{ Bugzilla::Group->match({ id => $params->{ids} }) };
}
Bugzilla->login(LOGIN_REQUIRED);
if ($isbless)
{
Bugzilla->user->in_group('editusers')
|| ThrowUserError("auth_failure", {
group => "editusers",
action => "edit",
object => "group"
});
}
else
{
Bugzilla->user->in_group('creategroups')
|| (!grep { !Bugzilla->user->can_bless($_->id) } @group_objects)
|| ThrowUserError("auth_failure", {
group => "creategroups",
action => "edit",
object => "group"
});
}
my @user_objects;
if ($params->{usernames})
{
push @user_objects, @{ Bugzilla::User->match({ login_name => $params->{usernames} }) };
}
if ($params->{userids})
{
push @user_objects, @{ Bugzilla::User->new_from_list($params->{userids}) };
}
@user_objects = grep { Bugzilla->user->can_see_user($_) } @user_objects;
@user_objects || ThrowCodeError('params_required', { function => 'Group.update', params => ['userids', 'usernames'] });
$add = $add ? 'add_user_groups' : 'remove_user_groups';
Bugzilla::Group->$add(
[ map { my $grp = $_; map { { group => $grp, user => $_ } } @user_objects } @group_objects ], $isbless
);
return {
groups => [ map { $self->_group_to_hash({}, $_) } @group_objects ],
users => [ map { user_to_hash($self, $_, {}) } @user_objects ],
};
}
sub _group_to_hash {
my ($self, $params, $group) = @_;
my $user = Bugzilla->user;
my $field_data = {
id => $self->type('int', $group->id),
name => $self->type('string', $group->name),
description => $self->type('string', $group->description),
};
if ($user->in_group('creategroups')) {
$field_data->{is_active} = $self->type('boolean', $group->is_active);
$field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
$field_data->{user_regexp} = $self->type('string', $group->user_regexp);
}
if ($params->{membership}) {
$field_data->{membership} = $self->_get_group_membership($group, $params);
}
return $field_data;
}
sub _get_group_membership {
my ($self, $group, $params) = @_;
my $user = Bugzilla->user;
my %users_only;
my $dbh = Bugzilla->dbh;
my $editusers = $user->in_group('editusers');
my $query = 'SELECT userid FROM profiles';
my $visibleGroups;
if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
# Show only users in visible groups.
$visibleGroups = $user->visible_groups_inherited;
if (scalar @$visibleGroups) {
$query .= qq{, user_group_map AS ugm
WHERE ugm.user_id = profiles.userid
AND ugm.isbless = 0
AND } . $dbh->sql_in('ugm.group_id', $visibleGroups);
}
} elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) {
$visibleGroups = 1;
$query .= qq{, user_group_map AS ugm
WHERE ugm.user_id = profiles.userid
AND ugm.isbless = 0
};
}
if (!$visibleGroups) {
ThrowUserError('group_not_visible', { group => $group });
}
my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
$query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
my $userids = $dbh->selectcol_arrayref($query);
my $user_objects = Bugzilla::User->new_from_list($userids);
my @users =
map {{
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_enabled),
email_enabled => $self->type('boolean', $_->email_enabled),
login_denied_text => $self->type('string', $_->disabledtext),
}} @$user_objects;
return \@users;
}
1;
__END__
=head1 NAME
Bugzilla::Webservice::Group - The API for creating, changing, and getting
information about Groups.
=head1 DESCRIPTION
This part of the Bugzilla API allows you to create Groups and
get information about them.
=head1 METHODS
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
the directions for how to access the data via REST is noted in each method
where applicable.
=head1 Group Creation and Modification
=head2 create
B<UNSTABLE>
=over
=item B<Description>
This allows you to create a new group in Bugzilla.
=item B<REST>
POST /rest/group
The params to include in the POST body as well as the returned data format,
are the same as below.
=item B<Params>
Some params must be set, or an error will be thrown. These params are
marked B<Required>.
=over
=item C<name>
B<Required> C<string> A short name for this group. Must be unique. This
is not usually displayed in the user interface, except in a few places.
=item C<description>
B<Required> C<string> A human-readable name for this group. Should be
relatively short. This is what will normally appear in the UI as the
name of the group.
=item C<user_regexp>
C<string> A regular expression. Any user whose Bugzilla username matches
this regular expression will automatically be granted membership in this group.
=item C<is_active>
C<boolean> C<True> if new group can be used for bugs, C<False> if this
is a group that will only contain users and no bugs will be restricted
to it.
=item C<icon_url>
C<string> A URL pointing to a small icon used to identify the group.
This icon will show up next to users' names in various parts of Bugzilla
if they are in this group.
=back
=item B<Returns>
A hash with one element, C<id>. This is the id of the newly-created group.
=item B<Errors>
=over
=item 800 (Empty Group Name)
You must specify a value for the C<name> field.
=item 801 (Group Exists)
There is already another group with the same C<name>.
=item 802 (Group Missing Description)
You must specify a value for the C<description> field.
=item 803 (Group Regexp Invalid)
You specified an invalid regular expression in the C<user_regexp> field.
=back
=item B<History>
=over
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head2 update
B<UNSTABLE>
=over
=item B<Description>
This allows you to update a group in Bugzilla.
=item B<REST>
PUT /rest/group/<group_name_or_id>
The params to include in the PUT body as well as the returned data format,
are the same as below. The C<ids> param will be overridden as it is pulled
from the URL path.
=item B<Params>
At least C<ids> or C<names> must be set, or an error will be thrown.
=over
=item C<ids>
B<Required> C<array> Contain ids of groups to update.
=item C<names>
B<Required> C<array> Contain names of groups to update.
=item C<name>
C<string> A new name for group.
=item C<description>
C<string> A new description for groups. This is what will appear in the UI
as the name of the groups.
=item C<user_regexp>
C<string> A new regular expression for email. Will automatically grant
membership to these groups to anyone with an email address that matches
this perl regular expression.
=item C<is_active>
C<boolean> Set if groups are active and eligible to be used for bugs.
True if bugs can be restricted to this group, false otherwise.
=item C<icon_url>
C<string> A URL pointing to an icon that will appear next to the name of
users who are in this group.
=back
=item B<Returns>
A C<hash> with a single field "groups". This points to an array of hashes
with the following fields:
=over
=item C<id>
C<int> The id of the group that was updated.
=item C<changes>
C<hash> The changes that were actually done on this group. The keys are
the names of the fields that were changed, and the values are a hash
with two keys:
=over
=item C<added>
C<string> The values that were added to this field,
possibly a comma-and-space-separated list if multiple values were added.
=item C<removed>
C<string> The values that were removed from this field, possibly a
comma-and-space-separated list if multiple values were removed.
=back
=back
=item B<Errors>
The same as L</create>.
=item B<History>
=over
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head1 Group Information
=head2 get
B<UNSTABLE>
=over
=item B<Description>
Returns information about L<Bugzilla::Group|Groups>.
=item B<REST>
To return information about a specific group by C<id> or C<name>:
GET /rest/group/<group_id_or_name>
You can also return information about more than one specific group
by using the following in your query string:
GET /rest/group?ids=1&ids=2&ids=3 or GET /group?names=ProductOne&names=Product2
the returned data format is same as below.
=item B<Params>
If neither ids or names is passed, and you are in the creategroups or
editusers group, then all groups will be retrieved. Otherwise, only groups
that you have bless privileges for will be returned.
=over
=item C<ids>
C<array> Contain ids of groups to update.
=item C<names>
C<array> Contain names of groups to update.
=item C<membership>
C<boolean> Set to 1 then a list of members of the passed groups' names and
ids will be returned.
=back
=item B<Returns>
If the user is a member of the "creategroups" group they will receive
information about all groups or groups matching the criteria that they passed.
You have to be in the creategroups group unless you're requesting membership
information.
If the user is not a member of the "creategroups" group, but they are in the
"editusers" group or have bless privileges to the groups they require
membership information for, the is_active, is_bug_group and user_regexp values
are not supplied.
The return value will be a hash containing group names as the keys, each group
name will point to a hash that describes the group and has the following items:
=over
=item id
C<int> The unique integer ID that Bugzilla uses to identify this group.
Even if the name of the group changes, this ID will stay the same.
=item name
C<string> The name of the group.
=item description
C<string> The description of the group.
=item is_bug_group
C<int> Whether this groups is to be used for bug reports or is only administrative specific.
=item user_regexp
C<string> A regular expression that allows users to be added to this group if their login matches.
=item is_active
C<int> Whether this group is currently active or not.
=item users
C<array> An array of hashes, each hash contains a user object for one of the
members of this group, only returned if the user sets the C<membership>
parameter to 1, the user hash has the following items:
=over
=item id
C<int> The id of the user.
=item real_name
C<string> The actual name of the user.
=item email
C<string> The email address of the user.
=item name
C<string> The login name of the user. Note that in some situations this is
different than their email.
=item can_login
C<boolean> A boolean value to indicate if the user can login into bugzilla.
=item email_enabled
C<boolean> A boolean value to indicate if bug-related mail will be sent
to the user or not.
=item disabled_text
C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled otherwise it is
disabled/closed.
=back
=back
=item B<Errors>
=over
=item 51 (Invalid Object)
A non existing group name was passed to the function, as a result no
group object existed for that invalid name.
=item 805 (Cannot view groups)
Logged-in users are not authorized to edit bugzilla groups as they are not
members of the creategroups group in bugzilla, or they are not authorized to
access group member's information as they are not members of the "editusers"
group or can bless the group.
=back
=item B<History>
=over
=item This function was added in Bugzilla B<5.0>.
=back
=back
=cut

File diff suppressed because it is too large Load Diff

View File

@ -1,62 +1,45 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Rosie Clarkson <rosie.clarkson@planningportal.gov.uk>
#
# Portions © Crown copyright 2009 - Rosie Clarkson (development@planningportal.gov.uk) for the Planning Portal
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Server::XMLRPC;
use 5.10.1;
use strict;
use SOAP::Transport::HTTP;
use warnings;
use XMLRPC::Transport::HTTP;
use Bugzilla::WebService::Server;
our @ISA = qw(Bugzilla::WebService::Server);
push @ISA, 'XMLRPC::Transport::HTTP::' . ($ENV{MOD_PERL} ? 'Apache' : 'CGI');
if ($ENV{MOD_PERL}) {
our @ISA = qw(XMLRPC::Transport::HTTP::Apache Bugzilla::WebService::Server);
} else {
our @ISA = qw(XMLRPC::Transport::HTTP::CGI Bugzilla::WebService::Server);
}
use Bugzilla::WebService::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
sub initialize {
my $self = shift;
my %retval = $self->SUPER::initialize(@_);
$retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
$retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
$retval{'dispatch_with'} = WS_DISPATCH;
return %retval;
}
use List::MoreUtils qw(none);
sub make_response {
my $self = shift;
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
foreach (@{Bugzilla->cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $_);
sub type {
my ($self, $type, $value) = @_;
if ($type eq 'dateTime') {
# This is the XML-RPC implementation, see the README in Bugzilla/WebService/.
# Our "base" implementation is in Bugzilla::WebService::Server.
$value = Bugzilla::WebService::Server->datetime_format_outbound($value);
$value =~ s/-//g;
}
}
sub handle_login {
my ($self, $classes, $action, $uri, $method) = @_;
my $class = $classes->{$uri};
my $full_method = $uri . "." . $method;
$self->SUPER::handle_login($class, $method, $full_method);
return;
elsif ($type eq 'email') {
$type = 'string';
if (Bugzilla->params->{'webservice_email_filter'}) {
$value = email_filter($value);
}
}
return XMLRPC::Data->type($type)->value($value);
}
# Patch SOAP::Transport::HTTP::CGI so it works under CGI like HTTP::Server::Simple
@ -93,20 +76,12 @@ sub handle_login {
print "HTTP/1.1 100 Continue\r\n\r\n";
}
#my $content = q{};
if ( !$chunked ) {
my $buffer;
binmode(STDIN);
if ( defined $ENV{'MOD_PERL'} ) {
while ( read( STDIN, $buffer, $length ) ) {
$content .= $buffer;
last if ( length($content) >= $length );
}
} else {
while ( sysread( STDIN, $buffer, $length ) ) {
$content .= $buffer;
last if ( length($content) >= $length );
}
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};
@ -147,22 +122,108 @@ sub handle_login {
$self->response->content;
};
sub initialize {
my $self = shift;
my %retval = $self->SUPER::initialize(@_);
$retval{'serializer'} = Bugzilla::XMLRPC::Serializer->new;
$retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
$retval{'dispatch_with'} = WS_DISPATCH;
return %retval;
}
sub make_response {
my $self = shift;
my $cgi = Bugzilla->cgi;
# Fix various problems with IIS.
if ($ENV{'SERVER_SOFTWARE'} =~ /IIS/) {
$ENV{CONTENT_LENGTH} = 0;
binmode(STDOUT, ':bytes');
}
$self->SUPER::make_response(@_);
# XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
# its cookies in Bugzilla::CGI, so we need to copy them over.
foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) {
$self->response->headers->push_header('Set-Cookie', $cookie);
}
# Copy across security related headers from Bugzilla::CGI
foreach my $header (split(/[\r\n]+/, $cgi->header)) {
my ($name, $value) = $header =~ /^([^:]+): (.*)/;
if ($name && !$self->response->headers->header($name)) {
$self->response->headers->header($name => $value);
}
}
}
sub handle_login {
my ($self, $classes, $action, $uri, $method) = @_;
my $class = $classes->{$uri};
my $full_method = $uri . "." . $method;
# Only allowed methods to be used from the module's whitelist
my $file = $class;
$file =~ s{::}{/}g;
$file .= ".pm";
require $file;
if (none { $_ eq $method } $class->PUBLIC_METHODS) {
ThrowCodeError('unknown_method', { method => $full_method });
}
$ENV{CONTENT_LENGTH} = 0 if $ENV{'SERVER_SOFTWARE'} =~ /IIS/;
$self->SUPER::handle_login($class, $method, $full_method);
return;
}
1;
# This exists to validate input parameters (which XMLRPC::Lite doesn't do)
# and also, in some cases, to more-usefully decode them.
package Bugzilla::XMLRPC::Deserializer;
use 5.10.1;
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
use warnings;
# We can't use "use parent" because XMLRPC::Serializer doesn't return
# a true value.
use XMLRPC::Lite;
our @ISA = qw(XMLRPC::Deserializer);
use Bugzilla::Error;
use Bugzilla::Constants qw(ERROR_MODE_DIE_SOAP_FAULT);
use Bugzilla::WebService::Constants qw(XMLRPC_CONTENT_TYPE_WHITELIST);
use Bugzilla::WebService::Util qw(fix_credentials);
use Scalar::Util qw(tainted);
sub new {
my $self = shift->SUPER::new(@_);
# Initialise XML::Parser to not expand references to entities, to prevent DoS
require XML::Parser;
my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } );
$self->{_parser}->parser($parser, $parser);
return $self;
}
sub deserialize {
# SOAP::Lite resets SIG{__DIE__} in the beginning of handle() method, so we set it again here
if ($Bugzilla::Error::HAVE_DEVEL_STACKTRACE) {
Bugzilla->error_mode(ERROR_MODE_DIE_SOAP_FAULT);
$SIG{__DIE__} = \&Bugzilla::_die_error;
}
my $self = shift;
# Only allow certain content types to protect against CSRF attacks
my $content_type = lc($ENV{'CONTENT_TYPE'});
# Remove charset, etc, if provided
$content_type =~ s/^([^;]+);.*/$1/;
if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) {
ThrowUserError('xmlrpc_illegal_content_type',
{ content_type => $ENV{'CONTENT_TYPE'} });
}
my ($xml) = @_;
my $som = $self->SUPER::deserialize(@_);
if (tainted($xml)) {
@ -172,7 +233,13 @@ sub deserialize {
my $params = $som->paramsin;
# This allows positional parameters for Testopia.
$params = {} if ref $params ne 'HASH';
# Update the params to allow for several convenience key/values
# use for authentication
fix_credentials($params);
Bugzilla->input_params($params);
return $som;
}
@ -236,7 +303,11 @@ sub _validation_subs {
1;
package Bugzilla::XMLRPC::SOM;
use 5.10.1;
use strict;
use warnings;
use XMLRPC::Lite;
our @ISA = qw(XMLRPC::SOM);
use Bugzilla::WebService::Util qw(taint_data);
@ -259,9 +330,13 @@ sub paramsin {
# This package exists to fix a UTF-8 bug in SOAP::Lite.
# See http://rt.cpan.org/Public/Bug/Display.html?id=32952.
package Bugzilla::XMLRPC::Serializer;
use Scalar::Util qw(blessed);
use 5.10.1;
use strict;
# We can't use "use base" because XMLRPC::Serializer doesn't return
use warnings;
use Scalar::Util qw(blessed reftype);
# We can't use "use parent" because XMLRPC::Serializer doesn't return
# a true value.
use XMLRPC::Lite;
our @ISA = qw(XMLRPC::Serializer);
@ -294,8 +369,8 @@ sub envelope {
my $self = shift;
my ($type, $method, $data) = @_;
# If the type isn't a successful response we don't want to change the values.
if ($type eq 'response'){
$data = _strip_undefs($data);
if ($type eq 'response') {
_strip_undefs($data);
}
return $self->SUPER::envelope($type, $method, $data);
}
@ -306,7 +381,9 @@ sub envelope {
# so it cannot be recursed like the other hash type objects.
sub _strip_undefs {
my ($initial) = @_;
if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
my $type = reftype($initial) or return;
if ($type eq "HASH") {
while (my ($key, $value) = each(%$initial)) {
if ( !defined $value
|| (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
@ -315,11 +392,11 @@ sub _strip_undefs {
delete $initial->{$key};
}
else {
$initial->{$key} = _strip_undefs($value);
_strip_undefs($value);
}
}
}
if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
elsif ($type eq "ARRAY") {
for (my $count = 0; $count < scalar @{$initial}; $count++) {
my $value = $initial->[$count];
if ( !defined $value
@ -330,11 +407,10 @@ sub _strip_undefs {
$count--;
}
else {
$initial->[$count] = _strip_undefs($value);
_strip_undefs($value);
}
}
}
return $initial;
}
sub BEGIN {
@ -436,3 +512,15 @@ perl-SOAP-Lite package in versions 0.68-1 and above.
=head1 SEE ALSO
L<Bugzilla::WebService>
=head1 B<Methods in need of POD>
=over
=item make_response
=item initialize
=item handle_login
=back

View File

@ -1,25 +1,14 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# Contributor(s): Marc Schumann <wurblzap@gmail.com>
# Max Kanat-Alexander <mkanat@bugzilla.org>
# Mads Bondo Dydensborg <mbd@dbc.dk>
# Noura Elhawary <nelhawar@redhat.com>
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::User;
use strict;
use warnings;
use base qw(Bugzilla::WebService);
use Bugzilla;
@ -27,8 +16,10 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
use Bugzilla::WebService::Util qw(filter validate);
use Bugzilla::Util qw(trim detaint_natural);
use Bugzilla::WebService::Util qw(filter filter_wants validate translate params_to_objects user_to_hash);
use List::Util qw(first min);
# Don't need auth to login
use constant LOGIN_EXEMPT => {
@ -40,44 +31,72 @@ use constant READ_ONLY => qw(
get
);
use constant PUBLIC_METHODS => qw(
create
get
login
logout
offer_account_by_email
update
valid_login
);
use constant MAPPED_FIELDS => {
email => 'login',
full_name => 'name',
login_denied_text => 'disabledtext',
};
use constant MAPPED_RETURNS => {
login_name => 'email',
realname => 'full_name',
disabledtext => 'login_denied_text',
};
##############
# User Login #
##############
sub login {
my ($self, $params) = @_;
my $remember = $params->{remember};
# Check to see if we are already logged in
my $user = Bugzilla->user;
if ($user->id) {
return $self->_login_to_hash($user);
}
# Username and password params are required
foreach my $param ("login", "password") {
defined $params->{$param}
(defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
|| ThrowCodeError('param_required', { param => $param });
}
# Convert $remember from a boolean 0/1 value to a CGI-compatible one.
if (defined($remember)) {
$remember = $remember? 'on': '';
}
else {
# Use Bugzilla's default if $remember is not supplied.
$remember =
Bugzilla->params->{'rememberlogin'} eq 'defaulton'? 'on': '';
}
# Make sure the CGI user info class works if necessary.
my $remember = $params->{Bugzilla_remember} || $params->{remember};
$remember = defined($remember) ? $remember : Bugzilla->params->{rememberlogin} eq 'defaulton';
my $input_params = Bugzilla->input_params;
$input_params->{'Bugzilla_login'} = $params->{login};
$input_params->{'Bugzilla_password'} = $params->{password};
$input_params->{'Bugzilla_remember'} = $remember;
$input_params->{Bugzilla_login} = $params->{Bugzilla_login} || $params->{login};
$input_params->{Bugzilla_password} = $params->{Bugzilla_password} || $params->{password};
$input_params->{Bugzilla_remember} = $remember ? 'on' : '';
Bugzilla->login();
return { id => $self->type('int', Bugzilla->user->id) };
$user = Bugzilla->login();
return $self->_login_to_hash($user);
}
sub logout {
my $self = shift;
Bugzilla->logout;
return undef;
}
sub valid_login {
my ($self, $params) = @_;
defined $params->{login}
|| ThrowCodeError('param_required', { param => 'login' });
Bugzilla->login();
if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
return $self->type('boolean', 1);
}
return $self->type('boolean', 0);
}
#################
@ -128,7 +147,9 @@ sub create {
# $call = $rpc->call( 'User.get', { match => [ 'testusera', 'testuserb' ],
# maxusermatches => 20, excludedisabled => 1 });
sub get {
my ($self, $params) = validate(@_, 'names', 'ids');
my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
Bugzilla->switch_to_shadow_db();
# Make them arrays if they aren't
if ($params->{names} && !ref $params->{names}) {
@ -168,11 +189,11 @@ sub get {
}
my $in_group = $self->_filter_users_by_group(
\@user_objects, $params);
@users = map {filter $params, {
@users = map { filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
name => $self->type('string', $_->login),
}} @$in_group;
real_name => $self->type('string', $_->realname),
name => $self->type('email', $_->login),
} } @$in_group;
return { users => \@users };
}
@ -180,7 +201,7 @@ sub get {
my $obj_by_ids;
$obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
# obj_by_ids are only visible to the user if he can see
# obj_by_ids are only visible to the user if they can see
# the otheruser, for non visible otheruser throw an error
foreach my $obj (@$obj_by_ids) {
if (Bugzilla->user->can_see_user($obj)){
@ -198,10 +219,15 @@ sub get {
}
# User Matching
my $limit;
if ($params->{'maxusermatches'}) {
$limit = $params->{'maxusermatches'} + 1;
my $limit = Bugzilla->params->{maxusermatches};
if ($params->{limit}) {
detaint_natural($params->{limit})
|| ThrowCodeError('param_must_be_numeric',
{ function => 'Bugzilla::WebService::User::match',
param => 'limit' });
$limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
}
foreach my $match_string (@{ $params->{'match'} || [] }) {
my $matched = Bugzilla::User::match_name($match_string, $limit, $params->{excludedisabled} && 1);
foreach my $user (@$matched) {
@ -212,34 +238,115 @@ sub get {
}
}
my $in_group = $self->_filter_users_by_group(
\@user_objects, $params);
if (Bugzilla->user->in_group('editusers')) {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->realname),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_enabled),
email_enabled => $self->type('boolean', $_->email_enabled),
login_denied_text => $self->type('string', $_->disabledtext),
}} @$in_group;
}
else {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->realname),
name => $self->type('string', $_->login),
email => $self->type('string', $_->email),
can_login => $self->type('boolean', $_->is_enabled),
}} @$in_group;
my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
foreach my $user (@$in_group) {
my $user_info = user_to_hash($self, $user);
if (Bugzilla->user->in_group('editusers')) {
$user_info->{email_enabled} = $self->type('boolean', $user->email_enabled);
$user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
}
if (Bugzilla->user->id == $user->id) {
if (filter_wants($params, 'saved_searches')) {
$user_info->{saved_searches} = [
map { $self->_query_to_hash($_) } @{ $user->queries }
];
}
if (filter_wants($params, 'saved_reports')) {
$user_info->{saved_reports} = [
map { $self->_report_to_hash($_) } @{ $user->reports }
];
}
}
if (filter_wants($params, 'groups')) {
if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
$user_info->{groups} = [
map { $self->_group_to_hash($_) } @{ $user->groups }
];
}
else {
$user_info->{groups} = $self->_filter_bless_groups($user->groups);
}
}
push(@users, $user_info);
}
return { users => \@users };
}
###############
# User Update #
###############
sub update {
my ($self, $params) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->login(LOGIN_REQUIRED);
# Reject access if there is no sense in continuing.
$user->in_group('editusers')
|| ThrowUserError("auth_failure", {group => "editusers",
action => "edit",
object => "users"});
defined($params->{names}) || defined($params->{ids})
|| ThrowCodeError('params_required',
{ function => 'User.update', params => ['ids', 'names'] });
my $user_objects = params_to_objects($params, 'Bugzilla::User');
my $values = translate($params, MAPPED_FIELDS);
# We delete names and ids to keep only new values to set.
delete $values->{names};
delete $values->{ids};
$dbh->bz_start_transaction();
foreach my $user (@$user_objects){
$user->set_all($values);
}
my %changes;
foreach my $user (@$user_objects){
my $returned_changes = $user->update();
$changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
}
$dbh->bz_commit_transaction();
my @result;
foreach my $user (@$user_objects) {
my %hash = (
id => $user->id,
changes => {},
);
foreach my $field (keys %{ $changes{$user->id} }) {
my $change = $changes{$user->id}->{$field};
# We normalize undef to an empty string, so that the API
# stays consistent for things that can become empty.
$change->[0] = '' if !defined $change->[0];
$change->[1] = '' if !defined $change->[1];
# We also flatten arrays (used by groups and blessed_groups)
$change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
$change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
$hash{changes}{$field} = {
removed => $self->type('string', $change->[0]),
added => $self->type('string', $change->[1])
};
}
push(@result, \%hash);
}
return { users => \@result };
}
sub _filter_users_by_group {
my ($self, $users, $params) = @_;
my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
@ -247,15 +354,23 @@ sub _filter_users_by_group {
# If no groups are specified, we return all users.
return $users if (!$group_ids and !$group_names);
my @groups = map { Bugzilla::Group->check({ id => $_ }) }
@{ $group_ids || [] };
my @name_groups = map { Bugzilla::Group->check($_) }
@{ $group_names || [] };
push(@groups, @name_groups);
my $user = Bugzilla->user;
my (@groups, %groups);
my @in_group = grep { $self->_user_in_any_group($_, \@groups) }
@$users;
if ($group_ids) {
@groups = map { Bugzilla::Group->check({ id => $_ }) } @$group_ids;
$groups{$_->id} = $_ foreach @groups;
}
if ($group_names) {
foreach my $name (@$group_names) {
my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' });
$user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name });
$groups{$group->id} = $group;
}
}
@groups = values %groups;
my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
return \@in_group;
}
@ -270,13 +385,65 @@ sub _user_in_any_group {
sub read_new_functionality {
my $time = time;
my $cgi = Bugzilla->cgi;
my @lu = map { $_ - 0} Bugzilla->params->{new_functionality_tsp} =~ m/(\d+)/g;
my $last_updated = POSIX::mktime(@lu[5], @lu[4], @lu[3], @lu[2], @lu[1] - 1, @lu[0] - 1900);
my @lu = map { $_ - 0 } Bugzilla->params->{new_functionality_tsp} =~ m/(\d+)/g;
my $last_updated = POSIX::mktime($lu[5], $lu[4], $lu[3], $lu[2], $lu[1] - 1, $lu[0] - 1900);
$time = $last_updated + 1 if $time < $last_updated;
$cgi->send_cookie('-name', 'read_new_functionality', '-value', $time);
return {status => 'ok'};
}
sub _filter_bless_groups {
my ($self, $groups) = @_;
my $user = Bugzilla->user;
my @filtered_groups;
foreach my $group (@$groups) {
next unless $user->can_bless($group->id);
push(@filtered_groups, $self->_group_to_hash($group));
}
return \@filtered_groups;
}
sub _group_to_hash {
my ($self, $group) = @_;
my $item = {
id => $self->type('int', $group->id),
name => $self->type('string', $group->name),
description => $self->type('string', $group->description),
};
return $item;
}
sub _query_to_hash {
my ($self, $query) = @_;
my $item = {
id => $self->type('int', $query->id),
name => $self->type('string', $query->name),
query => $self->type('string', $query->query),
};
return $item;
}
sub _report_to_hash {
my ($self, $report) = @_;
my $item = {
id => $self->type('int', $report->id),
name => $self->type('string', $report->name),
query => $self->type('string', $report->query),
};
return $item;
}
sub _login_to_hash {
my ($self, $user) = @_;
my $item = { id => $self->type('int', $user->id) };
if ($user->{_login_token}) {
$item->{'token'} = $user->id . "-" . $user->{_login_token};
}
return $item;
}
1;
__END__
@ -295,13 +462,19 @@ log in/out using an existing account.
See L<Bugzilla::WebService> for a description of how parameters are passed,
and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
=head2 Logging In and Out
Although the data input and output is the same for JSONRPC, XMLRPC and REST,
the directions for how to access the data via REST is noted in each method
where applicable.
=over
=head1 Logging In and Out
=item C<login>
These method are now deprecated, and will be removed in the release after
Bugzilla 5.0. The correct way of use these REST and RPC calls is noted in
L<Bugzilla::WebService>
B<STABLE>
=head2 login
B<DEPRECATED>
=over
@ -315,26 +488,23 @@ etc. This method logs in an user.
=over
=item C<login> (string) - The user's login name.
=item C<login> (string) - The user's login name.
=item C<password> (string) - The user's password.
=item C<remember> (bool) B<Optional> - if the cookies returned by the
call to login should expire with the session or not. In order for
this option to have effect the Bugzilla server must be configured to
allow the user to set this option - the Bugzilla parameter
I<rememberlogin> must be set to "defaulton" or
"defaultoff". Addionally, the client application must implement
management of cookies across sessions.
=item C<restrict_login> (bool) B<Optional> - If set to a true value,
the token returned by this method will only be valid from the IP address
which called this method.
=back
=item B<Returns>
On success, a hash containing one item, C<id>, the numeric id of the
user that was logged in. A set of http cookies is also sent with the
response. These cookies must be sent along with any future requests
to the webservice, for the duration of the session.
On success, a hash containing two items, C<id>, the numeric id of the
user that was logged in, and a C<token> which can be passed in
the parameters as authentication in other calls. The token can be sent
along with any future requests to the webservice, for the duration of the
session, i.e. till L<User.logout|/logout> is called.
=item B<Errors>
@ -344,15 +514,15 @@ to the webservice, for the duration of the session.
The username does not exist, or the password is wrong.
=item 301 (Account Disabled)
=item 301 (Login Disabled)
The account has been disabled. A reason may be specified with the
error.
The ability to login with this account has been disabled. A reason may be
specified with the error.
=item 305 (New Password Required)
The current password is correct, but the user is asked to change
his password.
their password.
=item 50 (Param Required)
@ -360,11 +530,26 @@ A login or password parameter was not provided.
=back
=item B<History>
=over
=item C<remember> was removed in Bugzilla B<5.0> as this method no longer
creates a login cookie.
=item C<restrict_login> was added in Bugzilla B<5.0>.
=item C<token> was added in Bugzilla B<4.4.3>.
=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
=back
=item C<logout>
=back
B<STABLE>
=head2 logout
B<DEPRECATED>
=over
@ -380,13 +565,55 @@ Log out the user. Does nothing if there is no user logged in.
=back
=back
=head2 valid_login
=head2 Account Creation
B<DEPRECATED>
=over
=item C<offer_account_by_email>
=item B<Description>
This method will verify whether a client's cookies or current login
token is still valid or have expired. A valid username must be provided
as well that matches.
=item B<Params>
=over
=item C<login>
The login name that matches the provided cookies or token.
=item C<token>
(string) Persistent login token current being used for authentication (optional).
Cookies passed by client will be used before the token if both provided.
=back
=item B<Returns>
Returns true/false depending on if the current cookies or token are valid
for the provided username.
=item B<Errors> (none)
=item B<History>
=over
=item Added in Bugzilla B<5.0>.
=item This function will be removed in the release after Bugzilla 5.0, in favour of API keys.
=back
=back
=head1 Account Creation and Modification
=head2 offer_account_by_email
B<STABLE>
@ -427,7 +654,7 @@ email address you specified. Account creation may be entirely disabled.
=back
=item C<create>
=head2 create
B<STABLE>
@ -443,6 +670,13 @@ actually receive an email. This function does not check that.
You must be logged in and have the C<editusers> privilege in order to
call this function.
=item B<REST>
POST /rest/user
The params to include in the POST body as well as the returned data format,
are the same as below.
=item B<Params>
=over
@ -486,17 +720,160 @@ password is under three characters.)
=item Error 503 (Password Too Long) removed in Bugzilla B<3.6>.
=back
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head2 User Info
=head2 update
B<EXPERIMENTAL>
=over
=item C<get>
=item B<Description>
Updates user accounts in Bugzilla.
=item B<REST>
PUT /rest/user/<user_id_or_name>
The params to include in the PUT body as well as the returned data format,
are the same as below. The C<ids> and C<names> params are overridden as they
are pulled from the URL path.
=item B<Params>
=over
=item C<ids>
C<array> Contains ids of user to update.
=item C<names>
C<array> Contains email/login of user to update.
=item C<full_name>
C<string> The new name of the user.
=item C<email>
C<string> The email of the user. Note that email used to login to bugzilla.
Also note that you can only update one user at a time when changing the
login name / email. (An error will be thrown if you try to update this field
for multiple users at once.)
=item C<password>
C<string> The password of the user.
=item C<email_enabled>
C<boolean> A boolean value to enable/disable sending bug-related mail to the user.
=item C<login_denied_text>
C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled otherwise it is
disabled/closed.
=item C<groups>
C<hash> These specify the groups that this user is directly a member of.
To set these, you should pass a hash as the value. The hash may contain
the following fields:
=over
=item C<add> An array of C<int>s or C<string>s. The group ids or group names
that the user should be added to.
=item C<remove> An array of C<int>s or C<string>s. The group ids or group names
that the user should be removed from.
=item C<set> An array of C<int>s or C<string>s. An exact set of group ids
and group names that the user should be a member of. NOTE: This does not
remove groups from the user where the person making the change does not
have the bless privilege for.
If you specify C<set>, then C<add> and C<remove> will be ignored. A group in
both the C<add> and C<remove> list will be added. Specifying a group that the
user making the change does not have bless rights will generate an error.
=back
=item C<bless_groups>
C<hash> - This is the same as groups, but affects what groups a user
has direct membership to bless that group. It takes the same inputs as
groups.
=back
=item B<Returns>
A C<hash> with a single field "users". This points to an array of hashes
with the following fields:
=over
=item C<id>
C<int> The id of the user that was updated.
=item C<changes>
C<hash> The changes that were actually done on this user. The keys are
the names of the fields that were changed, and the values are a hash
with two keys:
=over
=item C<added>
C<string> The values that were added to this field,
possibly a comma-and-space-separated list if multiple values were added.
=item C<removed>
C<string> The values that were removed from this field, possibly a
comma-and-space-separated list if multiple values were removed.
=back
=back
=item B<Errors>
=over
=item 51 (Bad Login Name)
You passed an invalid login name in the "names" array.
=item 304 (Authorization Required)
Logged-in users are not authorized to edit other users.
=back
=item B<History>
=over
=item REST API call added in Bugzilla B<5.0>.
=back
=back
=head1 User Info
=head2 get
B<STABLE>
@ -506,6 +883,18 @@ B<STABLE>
Gets information about user accounts in Bugzilla.
=item B<REST>
To get information about a single user:
GET /rest/user/<user_id_or_name>
To search for users by name, group using URL params same as below:
GET /rest/user
The returned data format is the same as below.
=item B<Params>
B<Note>: At least one of C<ids>, C<names>, or C<match> must be specified.
@ -538,9 +927,6 @@ Bugzilla itself. Users will be returned whose real name or login name
contains any one of the specified strings. Users that you cannot see will
not be included in the returned list.
Some Bugzilla installations have user-matching turned off, in which
case you will only be returned exact matches.
Most installations have a limit on how many matches are returned for
each string, which defaults to 1000 but can be changed by the Bugzilla
administrator.
@ -550,6 +936,13 @@ if they try. (This is to make it harder for spammers to harvest email
addresses from Bugzilla, and also to enforce the user visibility
restrictions that are implemented on some Bugzillas.)
=item C<limit> (int)
Limit the number of users matched by the C<match> parameter. If value
is greater than the system limit, the system limit will be used. This
parameter is only used when user matching using the C<match> parameter
is being performed.
=item C<group_ids> (array)
=item C<groups> (array)
@ -559,6 +952,14 @@ C<groups> is an array of names of groups that a user can be in.
If these are specified, they limit the return value to users who are
in I<any> of the groups specified.
=item C<include_disabled> (boolean)
By default, when using the C<match> parameter, disabled users are excluded
from the returned results unless their full username is identical to the
match string. Setting C<include_disabled> to C<true> will include disabled
users in the returned results even if their username doesn't fully match
the input string.
=back
=item B<Returns>
@ -601,10 +1002,79 @@ C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled. Otherwise it is
disabled/closed.
=item groups
C<array> An array of group hashes the user is a member of. If the currently
logged in user is querying their own account or is a member of the 'editusers'
group, the array will contain all the groups that the user is a
member of. Otherwise, the array will only contain groups that the logged in
user can bless. Each hash describes the group and contains the following items:
=over
=item id
C<int> The group id
=item name
C<string> The name of the group
=item description
C<string> The description for the group
=back
=item saved_searches
C<array> An array of hashes, each of which represents a user's saved search and has
the following keys:
=over
=item id
C<int> An integer id uniquely identifying the saved search.
=item name
C<string> The name of the saved search.
=item query
C<string> The CGI parameters for the saved search.
=back
=item saved_reports
C<array> An array of hashes, each of which represents a user's saved report and has
the following keys:
=over
=item id
C<int> An integer id uniquely identifying the saved report.
=item name
C<string> The name of the saved report.
=item query
C<string> The CGI parameters for the saved report.
=back
B<Note>: If you are not logged in to Bugzilla when you call this function, you
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
C<real_name>, C<email>, and C<can_login> items.
C<real_name>, C<email>, C<can_login>, and C<groups> items. The groups returned are
filtered based on your permission to bless each group.
The C<saved_searches> and C<saved_reports> items are only returned if you are
querying your own account, even if you are in the editusers group.
=back
@ -612,10 +1082,14 @@ C<real_name>, C<email>, and C<can_login> items.
=over
=item 51 (Bad Login Name or Group Name)
=item 51 (Bad Login Name or Group ID)
You passed an invalid login name in the "names" array or a bad
group name/id in the C<groups>/C<group_ids> arguments.
group ID in the C<group_ids> argument.
=item 52 (Invalid Parameter)
The value used must be an integer greater than zero.
=item 304 (Authorization Required)
@ -627,6 +1101,11 @@ wanted to get information about by user id.
Logged-out users cannot use the "ids" or "match" arguments to this
function.
=item 804 (Invalid Group Name)
You passed a group name in the C<groups> argument which either does not
exist or you do not belong to it.
=back
=item B<History>
@ -637,7 +1116,16 @@ function.
=item C<group_ids> and C<groups> were added in Bugzilla B<4.0>.
=back
=item C<include_disabled> was added in Bugzilla B<4.0>. Default
behavior for C<match> was changed to only return enabled accounts.
=item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now
illegal to pass a group name you don't belong to.
=item C<groups>, C<saved_searches>, and C<saved_reports> were added
in Bugzilla B<4.4>.
=item REST API call added in Bugzilla B<5.0>.
=back

View File

@ -1,26 +1,23 @@
# -*- Mode: perl; indent-tabs-mode: nil -*-
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# The contents of this file are subject to the Mozilla Public
# License Version 1.1 (the "License"); you may not use this file
# except in compliance with the License. You may obtain a copy of
# the License at http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS
# IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
# implied. See the License for the specific language governing
# rights and limitations under the License.
#
# The Original Code is the Bugzilla Bug Tracking System.
#
# The Initial Developer of the Original Code is Everything Solved, Inc.
# Portions created by the Initial Developer are Copyright (C) 2008
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Max Kanat-Alexander <mkanat@bugzilla.org>
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::WebService::Util;
use 5.10.1;
use strict;
use warnings;
use Bugzilla::Flag;
use Bugzilla::FlagType;
use Bugzilla::Error;
use Bugzilla::Util qw(list);
use Storable qw(dclone);
use base qw(Exporter);
# We have to "require", not "use" this, because otherwise it tries to
@ -28,36 +25,166 @@ use base qw(Exporter);
require Test::Taint;
our @EXPORT_OK = qw(
extract_flags
filter
filter_wants
taint_data
validate
translate
params_to_objects
user_to_hash
fix_credentials
);
sub filter ($$) {
my ($params, $hash) = @_;
sub extract_flags {
my ($flags, $bug, $attachment) = @_;
my (@new_flags, @old_flags);
my $flag_types = $attachment ? $attachment->flag_types : $bug->flag_types;
my $current_flags = $attachment ? $attachment->flags : $bug->flags;
# Copy the user provided $flags as we may call extract_flags more than
# once when editing multiple bugs or attachments.
my $flags_copy = dclone($flags);
foreach my $flag (@$flags_copy) {
my $id = $flag->{id};
my $type_id = $flag->{type_id};
my $new = delete $flag->{new};
my $name = delete $flag->{name};
if ($id) {
my $flag_obj = grep($id == $_->id, @$current_flags);
$flag_obj || ThrowUserError('object_does_not_exist',
{ class => 'Bugzilla::Flag', id => $id });
}
elsif ($type_id) {
my $type_obj = grep($type_id == $_->id, @$flag_types);
$type_obj || ThrowUserError('object_does_not_exist',
{ class => 'Bugzilla::FlagType', id => $type_id });
if (!$new) {
my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
@flag_matches > 1 && ThrowUserError('flag_not_unique',
{ value => $type_id });
if (!@flag_matches) {
delete $flag->{id};
}
else {
delete $flag->{type_id};
$flag->{id} = $flag_matches[0]->id;
}
}
}
elsif ($name) {
my @type_matches = grep($name eq $_->name, @$flag_types);
@type_matches > 1 && ThrowUserError('flag_type_not_unique',
{ value => $name });
@type_matches || ThrowUserError('object_does_not_exist',
{ class => 'Bugzilla::FlagType', name => $name });
if ($new) {
delete $flag->{id};
$flag->{type_id} = $type_matches[0]->id;
}
else {
my @flag_matches = grep($name eq $_->type->name, @$current_flags);
@flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name });
if (@flag_matches) {
$flag->{id} = $flag_matches[0]->id;
}
else {
delete $flag->{id};
$flag->{type_id} = $type_matches[0]->id;
}
}
}
if ($flag->{id}) {
push(@old_flags, $flag);
}
else {
push(@new_flags, $flag);
}
}
return (\@old_flags, \@new_flags);
}
sub filter($$;$$) {
my ($params, $hash, $types, $prefix) = @_;
my %newhash = %$hash;
foreach my $key (keys %$hash) {
delete $newhash{$key} if !filter_wants($params, $key);
delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
}
return \%newhash;
}
sub filter_wants ($$) {
my ($params, $field) = @_;
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
sub filter_wants($$;$$) {
my ($params, $field, $types, $prefix) = @_;
if (defined $params->{include_fields}) {
return 0 if !$include{$field};
}
if (defined $params->{exclude_fields}) {
return 0 if $exclude{$field};
# Since this is operation is resource intensive, we will cache the results
# This assumes that $params->{*_fields} doesn't change between calls
my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
$field = "${prefix}.${field}" if $prefix;
if (exists $cache->{$field}) {
return $cache->{$field};
}
return 1;
# Mimic old behavior if no types provided
my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
my %include = map { $_ => 1 } list($params->{include_fields});
my %exclude = map { $_ => 1 } list($params->{exclude_fields});
my %include_types;
my %exclude_types;
# Only return default fields if nothing is specified
$include_types{default} = 1 if !%include;
# Look for any field types requested
foreach my $key (keys %include) {
next if $key !~ /^_(.*)$/;
$include_types{$1} = 1;
delete $include{$key};
}
foreach my $key (keys %exclude) {
next if $key !~ /^_(.*)$/;
$exclude_types{$1} = 1;
delete $exclude{$key};
}
# Explicit inclusion/exclusion
return $cache->{$field} = 0 if $exclude{$field};
return $cache->{$field} = 1 if $include{$field};
# If the user has asked to include all or exclude all
return $cache->{$field} = 0 if $exclude_types{'all'};
return $cache->{$field} = 1 if $include_types{'all'};
# If the user has not asked for any fields specifically or if the user has asked
# for one or more of the field's types (and not excluded them)
foreach my $type (keys %field_types) {
return $cache->{$field} = 0 if $exclude_types{$type};
return $cache->{$field} = 1 if $include_types{$type};
}
my $wants = 0;
if ($prefix) {
# Include the field if the parent is include (and this one is not excluded)
$wants = 1 if $include{$prefix};
}
else {
# We want to include this if one of the sub keys is included
my $key = $field . '.';
my $len = length($key);
$wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
}
return $cache->{$field} = $wants;
}
sub taint_data {
@ -77,8 +204,9 @@ sub _delete_bad_keys {
# Making something a hash key always untaints it, in Perl.
# However, we need to validate our argument names in some way.
# We know that all hash keys passed in to the WebService will
# match \w+, so we delete any key that doesn't match that.
if ($key !~ /^\w+$/) {
# match \w+, contain '.' or '-', so we delete any key that
# doesn't match that.
if ($key !~ /^[\w\.\-]+$/) {
delete $item->{$key};
}
}
@ -108,6 +236,67 @@ sub validate {
return ($self, $params);
}
sub translate {
my ($params, $mapped) = @_;
my %changes;
while (my ($key,$value) = each (%$params)) {
my $new_field = $mapped->{$key} || $key;
$changes{$new_field} = $value;
}
return \%changes;
}
sub params_to_objects {
my ($params, $class) = @_;
my (@objects, @objects_by_ids);
@objects = map { $class->check($_) }
@{ $params->{names} } if $params->{names};
@objects_by_ids = map { $class->check({ id => $_ }) }
@{ $params->{ids} } if $params->{ids};
push(@objects, @objects_by_ids);
my %seen;
@objects = grep { !$seen{$_->id}++ } @objects;
return \@objects;
}
sub user_to_hash {
my ($ws, $user, $params) = @_;
return filter $params, {
id => $ws->type('int', $user->id),
real_name => $ws->type('string', $user->realname),
name => $ws->type('email', $user->login),
email => $ws->type('email', $user->email),
can_login => $ws->type('boolean', $user->is_enabled ? 1 : 0),
};
}
sub fix_credentials {
my ($params) = @_;
# Allow user to pass in login=foo&password=bar as a convenience
# even if not calling GET /login. We also do not delete them as
# GET /login requires "login" and "password".
if (exists $params->{'login'} && exists $params->{'password'}) {
$params->{'Bugzilla_login'} = delete $params->{'login'};
$params->{'Bugzilla_password'} = delete $params->{'password'};
}
# Allow user to pass api_key=12345678 as a convenience which becomes
# "Bugzilla_api_key" which is what the auth code looks for.
if (exists $params->{api_key}) {
$params->{Bugzilla_api_key} = delete $params->{api_key};
}
# Allow user to pass token=12345678 as a convenience which becomes
# "Bugzilla_token" which is what the auth code looks for.
if (exists $params->{'token'}) {
$params->{'Bugzilla_token'} = delete $params->{'token'};
}
# Allow extensions to modify the credential data before login
Bugzilla::Hook::process('webservice_fix_credentials', { params => $params });
}
__END__
=head1 NAME
@ -129,25 +318,64 @@ internally in the WebService code.
=head1 METHODS
=over
=item C<filter>
=head2 filter
This helps implement the C<include_fields> and C<exclude_fields> arguments
of WebService methods. Given a hash (the second argument to this subroutine),
this will remove any keys that are I<not> in C<include_fields> and then remove
any keys that I<are> in C<exclude_fields>.
=item C<filter_wants>
An optional third option can be passed that prefixes the field name to allow
filtering of data two or more levels deep.
For example, if you want to filter out the C<id> key/value in components returned
by Product.get, you would use the value C<component.id> in your C<exclude_fields>
list.
=head2 filter_wants
Returns C<1> if a filter would preserve the specified field when passing
a hash to L</filter>, C<0> otherwise.
=item C<validate>
=head2 validate
This helps in the validation of parameters passed into the WebSerice
This helps in the validation of parameters passed into the WebService
methods. Currently it converts listed parameters into an array reference
if the client only passed a single scalar value. It modifies the parameters
hash in place so other parameters should be unaltered.
=head2 translate
WebService methods frequently take parameters with different names than
the ones that we use internally in Bugzilla. This function takes a hashref
that has field names for keys and returns a hashref with those keys renamed
according to the mapping passed in with the second parameter (which is also
a hashref).
=head2 params_to_objects
Creates objects of the type passed in as the second parameter, using the
parameters passed to a WebService method (the first parameter to this function).
Helps make life simpler for WebService methods that internally create objects
via both "ids" and "names" fields. Also de-duplicates objects that were loaded
by both "ids" and "names". Returns an arrayref of objects.
=head2 fix_credentials
Allows for certain parameters related to authentication such as Bugzilla_login,
Bugzilla_password, and Bugzilla_token to have shorter named equivalents passed in.
This function converts the shorter versions to their respective internal names.
=head2 extract_flags
Subroutine that takes a list of hashes that are potential flag changes for
both bugs and attachments. Then breaks the list down into two separate lists
based on if the change is to add a new flag or to update an existing flag.
=head1 B<Methods in need of POD>
=over
=item taint_data
=back

View File

@ -40,6 +40,7 @@ use constant DB_COLUMNS => qw(
sortkey
onemailperbug
title
isreport
);
use constant NAME_FIELD => 'id';
@ -53,7 +54,7 @@ sub sortkey { return $_[0]->{'sortkey'}; }
sub one_email_per_bug { return $_[0]->{'onemailperbug'}; }
sub title { return $_[0]->{'title'}; }
sub name { return $_[0]->{'query_name'}; }
sub isreport { return $_[0]->{'isreport'}; }
1;

View File

@ -1,10 +1,41 @@
== UNRELEASED: beta ==
== branch: i18n ==
UI improvements:
* Separate localisation layer - now it's possible to translate Bugzilla UI
without copying templates!
== UNRELEASED: beta ==
== 2016.09: 2016-09-01, commit 55bee6cdccae3a83200c799a23a4fda4d135717f ==
Backports from original Bugzilla:
* WebServices from Bugzilla 5.0.1 (everything except tags).
Other new features:
* Separate "createproducts" role (group) that only allows to create new products,
but not to edit the existing ones.
* Support custom saved search columns in whining
* Support saved reports in whining
* Support multi-select columns of bugs referenced by bug type custom fields in search
* Check saved search runner permissions correctly in "matched by saved search" operator when query is shared
* Report all XML-RPC exceptions
* Add Group.{add,remove}_{members,managers} web services
* Add "bypass group" parameters to Checkers
* Support multiple "look for bug in..." URLs
Bugfixes:
* Whining now works again.
* Fix&rework whining
* Silence CGI.pm warnings
* Fix `deadline` field history logging
* Fix "totals" in table reports
* Do not crash in Search when multiple values are passed to a single-value operator
* Several other fixes
== 2015.09: 2015-09-18, commit dc07c69094f616d1eb3e523181283dcdbca0499e ==
UI improvements:

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(.);
@ -120,6 +118,7 @@ sub new
{
for (glob $script)
{
($_) = /^(.*)$/so;
eval
{
$self->load_script($_);
@ -252,6 +251,7 @@ sub run_script
my ($script) = @_;
$self->load_script($script);
my $start = [gettimeofday];
$Bugzilla::Error::IN_EVAL++;
$in_eval = 1;
eval { &{$subs{$script}}(); };
$self->check_errors($script);
@ -289,6 +289,7 @@ sub check_errors
print STDERR "Error in _cleanup():\n$@";
}
$in_eval = 0;
$Bugzilla::Error::IN_EVAL--;
if ($err)
{
$self->internal_error($err);

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

@ -515,6 +515,7 @@ sub enter
my $comment = $ARGS->{comment};
$comment = '' unless defined $comment;
$vars->{commenttext} = $comment;
$vars->{work_time} = $ARGS->{work_time};
# Generate and return the UI (HTML page) from the appropriate template.
Bugzilla->template->process("attachment/create.html.tmpl", $vars)

View File

@ -183,27 +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};
my $order = $ARGS->{order} || "";
&& $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;
@ -213,36 +201,17 @@ my $params;
if (defined $ARGS->{regetlastlist})
{
my $bug_id = Bugzilla->cookies->{BUGLIST} || ThrowUserError('missing_cookie');
$order = 'reuse last sort' unless $order;
$ARGS->{order} ||= 'reuse last sort';
$bug_id =~ s/:/,/g;
# set up the params for this new query
$params = {
bug_id => $bug_id,
bug_id_type => 'anyexact',
order => $order,
order => $ARGS->{order},
columnlist => $ARGS->{columnlist},
};
}
# Figure out whether or not the user is doing a fulltext search. If not,
# we'll remove the relevance column from the lists of columns to display
# and order by, since relevance only exists when doing a fulltext search.
my $fulltext = 0;
if ($ARGS->{content})
{
$fulltext = 1;
}
my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), keys %$ARGS);
foreach my $chart (@charts)
{
if ($ARGS->{"field$chart"} eq 'content' && $ARGS->{"value$chart"})
{
$fulltext = 1;
last;
}
}
################################################################################
# Utilities
################################################################################
@ -458,14 +427,12 @@ if ($cmdtype eq "dorem")
$vars->{search_id} = $query->id;
}
$params = http_decode_query($query->query.'&sharer_id='.$query->userid);
$order = $params->{order} || $order;
}
elsif ($remaction eq "runseries")
{
$vars->{searchname} = $ARGS->{namedcmd};
$vars->{searchtype} = "series";
$params = http_decode_query(LookupSeries($ARGS->{"series_id"}));
$order = $params->{order} || $order;
}
elsif ($remaction eq 'forget')
{
@ -524,117 +491,38 @@ elsif (($cmdtype eq 'doit') && defined $ARGS->{remtype})
}
}
################################################################################
# Column Definition
################################################################################
my $columns = Bugzilla::Search::COLUMNS;
################################################################################
# Display Column Determination
################################################################################
# Determine the columns that will be displayed in the bug list via the
# columnlist CGI parameter, the user's preferences, or the default.
my @displaycolumns = ();
if (defined $params->{columnlist} && $params->{columnlist} ne 'all')
{
@displaycolumns = split(/[ ,]+/, $params->{columnlist});
}
elsif (defined Bugzilla->cookies->{COLUMNLIST})
{
@displaycolumns = split(/ /, Bugzilla->cookies->{COLUMNLIST});
}
else
{
# Use the default list of columns.
@displaycolumns = DEFAULT_COLUMN_LIST;
}
$_ = Bugzilla::Search->COLUMN_ALIASES->{$_} || $_ for @displaycolumns;
# Weed out columns that don't actually exist to prevent the user
# from hacking their column list cookie to grab data to which they
# should not have access. Detaint the data along the way.
@displaycolumns = grep($columns->{$_} && trick_taint($_), @displaycolumns);
my ($displaycolumns, $selectcolumns) = Bugzilla::Search->get_columns($params, Bugzilla->user);
# Add the votes column to the list of columns to be displayed
# in the bug list if the user is searching for bugs with a certain
# number of votes and the votes column is not already on the list.
# Some versions of perl will taint 'votes' if this is done as a single
# statement, because the votes param is tainted at this point
my $votes = $params->{votes};
$votes ||= "";
if (trim($votes) && !grep($_ eq 'votes', @displaycolumns))
if (trim($params->{votes} || '') && !grep($_ eq 'votes', @$selectcolumns))
{
push(@displaycolumns, 'votes');
push @$displaycolumns, 'votes';
push @$selectcolumns, 'votes';
}
# Remove the timetracking columns if they are not a part of the group
# (happens if a user had access to time tracking and it was revoked/disabled)
if (!Bugzilla->user->is_timetracker)
if ($superworktime && !grep($_ eq 'interval_time', @$selectcolumns))
{
@displaycolumns = grep { !TIMETRACKING_FIELDS->{$_} } @displaycolumns;
}
# Remove the relevance column if the user is not doing a fulltext search.
if (grep('relevance', @displaycolumns) && !$fulltext)
{
@displaycolumns = grep($_ ne 'relevance', @displaycolumns);
}
# Remove the "ID" column from the list because bug IDs are always displayed
# and are hard-coded into the display templates.
@displaycolumns = grep($_ ne 'bug_id', @displaycolumns);
if ($superworktime && !grep($_ eq 'interval_time', @displaycolumns))
{
push @displaycolumns, 'interval_time';
}
################################################################################
# Select Column Determination
################################################################################
# Generate the list of columns that will be selected in the SQL query.
# The bug ID is always selected because bug IDs are always displayed.
# Severity, priority, resolution and status are required for buglist
# CSS classes.
my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status", "resolution", "product");
# Make sure that the login_name version of a field is always also
# requested if the realname version is requested, so that we can
# display the login name when the realname is empty.
my @realname_fields = grep(/_realname$|_short$/, @displaycolumns);
foreach my $item (@realname_fields)
{
my $login_field = $item;
$login_field =~ s/_realname$|_short$//;
if (!grep($_ eq $login_field, @selectcolumns))
{
push(@selectcolumns, $login_field);
}
}
# Display columns are selected because otherwise we could not display them.
foreach my $col (@displaycolumns)
{
push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
push @$displaycolumns, 'interval_time';
push @$selectcolumns, 'interval_time';
}
# If the user is editing multiple bugs, we also make sure to select the
# status, because the values of that field determines what options the user
# has for modifying the bugs.
if ($dotweak)
if ($dotweak && !grep($_ eq 'bug_status', @$selectcolumns))
{
push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
push @$selectcolumns, "bug_status";
}
if ($format->{extension} eq 'ics')
if ($format->{extension} eq 'ics' && !grep($_ eq 'creation_ts', @$selectcolumns))
{
push(@selectcolumns, "creation_ts") if !grep($_ eq 'creation_ts', @selectcolumns);
push @$selectcolumns, "creation_ts";
}
if ($format->{extension} eq 'atom')
@ -662,102 +550,26 @@ if ($format->{extension} eq 'atom')
foreach my $required (@required_atom_columns)
{
push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns);
push(@$selectcolumns, $required) if !grep($_ eq $required, @$selectcolumns);
}
}
if ($superworktime && !grep($_ eq 'product_notimetracking', @displaycolumns))
if ($superworktime && !grep($_ eq 'product_notimetracking', @$displaycolumns))
{
push @selectcolumns, 'product_notimetracking';
push @$selectcolumns, 'product_notimetracking';
}
################################################################################
# Sort Order Determination
################################################################################
# Add to the query some instructions for sorting the bug list.
# First check if we'll want to reuse the last sorting order; that happens if
# the order is not defined or its value is "reuse last sort"
if (!$order || $order =~ /^reuse/i)
my ($orderstrings, $invalid_fragments) = Bugzilla::Search->get_order($params, Bugzilla->user);
if (scalar @$invalid_fragments)
{
if ($order = Bugzilla->cookies->{LASTORDER})
{
# Cookies from early versions of Specific Search included this text,
# which is now invalid.
$order =~ s/ LIMIT 200//;
}
else
{
$order = ''; # Remove possible "reuse" identifier as unnecessary
}
$vars->{message} = 'invalid_column_name';
$vars->{invalid_fragments} = $invalid_fragments;
}
# FIXME Move sort orders to Bugzilla::Search
my $old_orders = {
'' => 'bug_status,priority,assigned_to,bug_id', # Default
'bug number' => 'bug_id',
'importance' => 'priority,bug_severity,bug_id',
'assignee' => 'assigned_to,bug_status,priority,bug_id',
'last changed' => 'delta_ts,bug_status,priority,assigned_to,bug_id',
};
if ($order)
{
# Convert the value of the "order" form field into a list of columns
# by which to sort the results.
if ($old_orders->{lc $order})
{
$order = $old_orders->{lc $order};
}
else
{
my (@order, @invalid_fragments);
# A custom list of columns. Make sure each column is valid.
foreach my $fragment (split(/,/, $order))
{
$fragment = trim($fragment);
next unless $fragment;
my ($column_name, $direction) = Bugzilla::Search::split_order_term($fragment);
$column_name = Bugzilla::Search::translate_old_column($column_name);
# Special handlings for certain columns
next if $column_name eq 'relevance' && !$fulltext;
# If we are sorting by votes, sort in descending order if
# no explicit sort order was given.
if ($column_name eq 'votes' && !$direction)
{
$direction = "DESC";
}
if (exists $columns->{$column_name})
{
$direction = " $direction" if $direction;
push @order, "$column_name$direction";
}
else
{
push @invalid_fragments, $fragment;
}
}
if (scalar @invalid_fragments)
{
$vars->{message} = 'invalid_column_name';
$vars->{invalid_fragments} = \@invalid_fragments;
}
$order = join(",", @order);
# Now that we have checked that all columns in the order are valid,
# detaint the order string.
trick_taint($order) if $order;
}
}
$order = $old_orders->{''} if !$order;
my @orderstrings = split(/,\s*/, $order);
# The bug status defined by a specific search is of type __foo__, but
# Search.pm converts it into a list of real bug statuses, which cannot
# be used when editing the specific search again. So we restore this
@ -770,9 +582,9 @@ if ($query_format eq 'specific')
# Generate the basic SQL query that will be used to generate the bug list.
my $search = new Bugzilla::Search(
'fields' => \@selectcolumns,
'fields' => $selectcolumns,
'params' => $params,
'order' => \@orderstrings
'order' => $orderstrings,
);
my $query = $search->getSQL();
$vars->{search_description} = $search->search_description_html;
@ -798,12 +610,9 @@ if (defined $ARGS->{limit})
$query .= " " . $dbh->sql_limit($limit);
}
}
elsif ($fulltext)
elsif ($ARGS->{order} && $ARGS->{order} =~ /^relevance/)
{
if ($ARGS->{order} && $ARGS->{order} =~ /^relevance/)
{
$vars->{message} = 'buglist_sorted_by_relevance';
}
$vars->{message} = 'buglist_sorted_by_relevance';
}
if ($superworktime)
@ -898,22 +707,16 @@ $buglist_sth->execute();
# Retrieve the query results one row at a time and write the data into a list of Perl records.
# If we're doing time tracking, then keep totals for all bugs.
my $percentage_complete = 1 && grep { $_ eq 'percentage_complete' } @displaycolumns;
my $estimated_time = 1 && grep { $_ eq 'estimated_time' } @displaycolumns;
my $remaining_time = $percentage_complete || grep { $_ eq 'remaining_time' } @displaycolumns;
my $work_time = $percentage_complete || grep { $_ eq 'work_time' } @displaycolumns;
my $interval_time = $percentage_complete || grep { $_ eq 'interval_time' } @displaycolumns;
my $time_info = {
estimated_time => 0,
remaining_time => 0,
work_time => 0,
percentage_complete => 0,
interval_time => 0, # CustIS Bug 68921
time_present => ($estimated_time || $remaining_time ||
$work_time || $percentage_complete || $interval_time),
};
# Calculate totals
my $total_info;
for my $column (@$displaycolumns)
{
if (Bugzilla::Search->COLUMNS->{$column}->{numeric})
{
$total_info ||= {};
$total_info->{$column} = 0;
}
}
my $bugowners = {};
my $bugproducts = {};
@ -929,7 +732,7 @@ while (my @row = $buglist_sth->fetchrow_array())
# Slurp the row of data into the record.
# The second from last column in the record is the number of groups
# to which the bug is restricted.
foreach my $column (@selectcolumns)
foreach my $column (@$selectcolumns)
{
$bug->{$column} = shift @row;
}
@ -938,12 +741,6 @@ while (my @row = $buglist_sth->fetchrow_array())
if ($bug->{delta_ts})
{
$bug->{delta_ts} =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
$bug->{delta_ts_diff} = DiffDate($bug->{delta_ts});
}
if ($bug->{creation_ts})
{
$bug->{creation_ts_diff} = DiffDate($bug->{creation_ts});
}
# Record the assignee, product, and status in the big hashes of those things.
@ -960,10 +757,10 @@ while (my @row = $buglist_sth->fetchrow_array())
push(@bugidlist, $bug->{bug_id});
# Compute time tracking info.
$time_info->{estimated_time} += $bug->{estimated_time} if $estimated_time;
$time_info->{remaining_time} += $bug->{remaining_time} if $remaining_time;
$time_info->{work_time} += $bug->{work_time} if $work_time;
$time_info->{interval_time} += $bug->{interval_time} if $interval_time;
for my $column (keys %$total_info)
{
$total_info->{$column} += $bug->{$column} if $column ne 'percentage_complete';
}
}
my $query_template_time = gettimeofday();
@ -1003,15 +800,18 @@ if (@bugidlist)
}
# Compute percentage complete without rounding.
my $sum = $time_info->{work_time} + $time_info->{remaining_time};
if ($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
}
}
################################################################################
@ -1023,8 +823,7 @@ else
$vars->{bugs} = \@bugs;
$vars->{buglist} = \@bugidlist;
$vars->{buglist_joined} = join(',', @bugidlist);
$vars->{columns} = $columns;
$vars->{displaycolumns} = \@displaycolumns;
$vars->{displaycolumns} = $displaycolumns;
$vars->{openstates} = [ map { $_->name } grep { $_->is_open } Bugzilla::Status->get_all ];
# used by list.ics.tmpl
@ -1070,12 +869,12 @@ if ($vars->{urlquerypart}->{sharer_id})
}
$vars->{urlquerypart} = http_build_query($vars->{urlquerypart});
$vars->{rssquerypart} = $vars->{urlquerypart};
$vars->{order} = $order;
$vars->{order_columns} = [ @orderstrings ];
$vars->{order} = $params->{order};
$vars->{order_columns} = $orderstrings;
$vars->{order_dir} = [ map { s/ DESC$// ? 1 : 0 } @{$vars->{order_columns}} ];
$vars->{caneditbugs} = 1;
$vars->{time_info} = $time_info;
$vars->{total_info} = $total_info;
$vars->{query_params} = { %$params }; # now used only in superworktime
$vars->{query_params}->{chfieldfrom} = $search->{interval_from};
@ -1179,6 +978,7 @@ if ($dotweak && scalar @bugs)
my $visible = {};
for my $field (Bugzilla->active_custom_fields)
{
next if $field->type == FIELD_TYPE_BUG_ID_REV;
my $vis_field = $field->visibility_field;
my $vis = 1;
if ($vis_field)
@ -1231,8 +1031,7 @@ sub get_bug_vals
my $field_name = $field->name;
my $type = $field->value_type;
my $id_field = $type->ID_FIELD;
my $m = $field_name;
$m .= '_obj' if $field_name eq 'product' || $field_name eq 'component'; # FIXME remove when product_obj replaces product
my $m = $field_name.'_obj';
my $ids = {};
my $v;
for my $bug (@$bugs)
@ -1257,6 +1056,7 @@ $vars->{query_sql_time} = sprintf("%.2f", $query_sql_time);
Bugzilla::Hook::process('after-buglist', { vars => $vars });
# FIXME: Remove this hardcode
$vars->{abbrev} = {
bug_severity => { maxlength => 3, title => "Sev" },
priority => { maxlength => 3, title => "Pri" },
@ -1271,8 +1071,8 @@ $vars->{abbrev} = {
resolution => { maxlength => 4 },
short_short_desc => { maxlength => 60, ellipsis => "..." },
status_whiteboard => { title => "Whiteboard" },
component => { maxlength => 8, title => "Comp" },
product => { maxlength => 8 },
component => { maxlength => 20, ellipsis => "...", title => "Comp" },
product => { maxlength => 20, ellipsis => "..." },
op_sys => { maxlength => 4 },
target_milestone => { title => "Milestone" },
percentage_complete => { format_value => "%d %%" },
@ -1291,11 +1091,11 @@ my $disposition = "inline";
if ($format->{extension} eq "html" && !$agent)
{
if ($order && !$ARGS->{sharer_id} && $query_format ne 'specific')
if ($ARGS->{order} && !$ARGS->{sharer_id} && $query_format ne 'specific')
{
$cgi->send_cookie(
-name => 'LASTORDER',
-value => $order,
-value => $ARGS->{order},
-expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
);
}

View File

@ -31,6 +31,7 @@ use Bugzilla::Keyword;
use Bugzilla::Product;
use Bugzilla::Status;
use Bugzilla::Field;
use Bugzilla::Util qw(list);
use Digest::MD5 qw(md5_base64);

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

@ -37,8 +37,20 @@ if ($params->{save})
{
if (/^except_field_(\d+)$/so && $params->{$_})
{
$except->{$params->{$_}} =
$params->{"except_field_$1_value"} || undef;
my ($f, $v) = ($params->{$_}, $params->{"except_field_$1_value"});
if (!$v)
{
$except->{$f} = undef;
}
elsif (!exists $except->{$f})
{
$except->{$f} = $v;
}
elsif (defined $except->{$f})
{
$except->{$f} = [ $except->{$f} ] if !ref $except->{$f};
push @{$except->{$f}}, $v;
}
}
}
$except = undef if !%$except;
@ -72,12 +84,14 @@ if ($params->{save})
flags => $flags,
except_fields => $except,
triggers => $triggers,
bypass_group_id => $params->{bypass_group_id},
});
}
else
{
$ch = Bugzilla::Checker->check({ id => $id });
$ch->set_query_id($params->{query_id});
$ch->set('query_id', $params->{query_id});
$ch->set('bypass_group_id', $params->{bypass_group_id});
$ch->set_message($params->{message});
$ch->set_flags($flags);
$ch->set_except_fields($except);
@ -106,6 +120,7 @@ else
{
$vars->{token} = issue_session_token('editcheckers');
$vars->{create} = $params->{create} ? 1 : 0;
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
# Есть специальное поле "longdesc", означающее добавление комментариев
my $f = [ Bugzilla->get_fields ];
@$f = sort { lc $a->description cmp lc $b->description } grep { $_->name !~ /

View File

@ -82,7 +82,7 @@ LoadTemplate('select') unless $action;
if ($action eq 'add')
{
$vars->{token} = issue_session_token('add_classification');
LoadTemplate($action);
LoadTemplate('edit');
}
#
@ -127,7 +127,7 @@ if ($action eq 'del')
$vars->{classification} = $classification;
$vars->{token} = issue_session_token('delete_classification');
LoadTemplate($action);
LoadTemplate('del');
}
#
@ -158,7 +158,7 @@ if ($action eq 'edit')
$vars->{classification} = $classification;
$vars->{token} = issue_session_token('edit_classification');
LoadTemplate($action);
LoadTemplate('edit');
}
#
@ -224,7 +224,7 @@ if ($action eq 'reclassify')
$vars->{classification} = $classification;
$vars->{token} = issue_session_token('reclassify_classifications');
LoadTemplate($action);
LoadTemplate('reclassify');
}
#

View File

@ -148,7 +148,7 @@ elsif ($action eq 'update')
}
else
{
$field->${\$_->[2]}([ list $ARGS->{$_->[3]} ]);
$field->${\$_->[2]}([ list $ARGS->{$_->[3]} ], 'SKIP_INVISIBLE');
}
}
}

View File

@ -106,22 +106,22 @@ sub get_current_and_available
push @bless_to_available, $group_option if !$bless_to_current->{$group_option->id};
}
$vars->{members_current} = [ values %$members_current ];
$vars->{members_available} = \@members_available;
$vars->{member_of_current} = [ values %$member_of_current ];
$vars->{member_of_available} = \@member_of_available;
$vars->{members_current} = [ sort { $a->name cmp $b->name } values %$members_current ];
$vars->{members_available} = [ sort { $a->name cmp $b->name } @members_available ];
$vars->{member_of_current} = [ sort { $a->name cmp $b->name } values %$member_of_current ];
$vars->{member_of_available} = [ sort { $a->name cmp $b->name } @member_of_available ];
$vars->{bless_from_current} = [ values %$bless_from_current ];
$vars->{bless_from_available} = \@bless_from_available;
$vars->{bless_to_current} = [ values %$bless_to_current ];
$vars->{bless_to_available} = \@bless_to_available;
$vars->{bless_from_current} = [ sort { $a->name cmp $b->name } values %$bless_from_current ];
$vars->{bless_from_available} = [ sort { $a->name cmp $b->name } @bless_from_available ];
$vars->{bless_to_current} = [ sort { $a->name cmp $b->name } values %$bless_to_current ];
$vars->{bless_to_available} = [ sort { $a->name cmp $b->name } @bless_to_available ];
if (Bugzilla->params->{usevisibilitygroups})
{
$vars->{visible_from_current} = [ values %$visible_from_current ];
$vars->{visible_from_available} = \@visible_from_available;
$vars->{visible_to_me_current} = [ values %$visible_to_me_current ];
$vars->{visible_to_me_available} = \@visible_to_me_available;
$vars->{visible_from_current} = [ sort { $a->name cmp $b->name } values %$visible_from_current ];
$vars->{visible_from_available} = [ sort { $a->name cmp $b->name } @visible_from_available ];
$vars->{visible_to_me_current} = [ sort { $a->name cmp $b->name } values %$visible_to_me_current ];
$vars->{visible_to_me_available} = [ sort { $a->name cmp $b->name } @visible_to_me_available ];
}
}
@ -257,6 +257,8 @@ if ($action eq 'delete')
Bugzilla::Hook::process('editgroups-post_delete', { group => $group });
Bugzilla::Views::refresh_some_views();
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
$vars->{message} = 'group_deleted';
ListGroups($vars);
@ -273,6 +275,8 @@ if ($action eq 'postchanges')
Bugzilla::Hook::process('editgroups-post_edit', {});
Bugzilla::Views::refresh_some_views();
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
delete_token($token);
@ -302,6 +306,7 @@ if ($action eq 'confirm_remove')
if ($action eq 'remove_regexp')
{
check_token_data($token, 'remove_group_members');
# remove all explicit users from the group with
# gid = $ARGS->{group} that match the regular expression
# stored in the DB for that group or all of them period
@ -310,26 +315,17 @@ if ($action eq 'remove_regexp')
my $regexp = CheckGroupRegexp($ARGS->{regexp});
$dbh->bz_start_transaction();
my $users = $group->members_direct();
my $sth_delete = $dbh->prepare("DELETE FROM user_group_map WHERE user_id = ? AND isbless = 0 AND group_id = ?");
my @deleted;
foreach my $member (@$users)
{
if ($regexp eq '' || $member->login =~ m/$regexp/i)
{
$sth_delete->execute($member->id, $group->id);
push @deleted, $member;
}
}
my $del = [ grep { $_->login =~ m/$regexp/is } @{ $group->members_direct } ];
$group->remove_users($del, 0);
$dbh->bz_commit_transaction();
$vars->{users} = \@deleted;
$vars->{users} = $del;
$vars->{regexp} = $regexp;
Bugzilla::Hook::process('editgroups-post_remove_regexp', { deleted => \@deleted });
Bugzilla::Hook::process('editgroups-post_remove_regexp', { deleted => $del });
Bugzilla::Views::refresh_some_views();
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
delete_token($token);

View File

@ -50,6 +50,7 @@ my $vars = {};
$vars->{doc_section} = 'products.html';
$user->in_group('editcomponents') ||
$user->in_group('createproducts') ||
scalar(@{$user->get_editable_products}) ||
ThrowUserError('auth_failure', {
group => 'editcomponents',
@ -71,12 +72,12 @@ my $useclassification = Bugzilla->get_field('classification')->enabled;
# classifications enabled)
#
if ($useclassification && !$classification_name && !$product_name)
if (!$action && $useclassification && !$classification_name && !$product_name)
{
my $class;
if ($user->in_group('editcomponents'))
{
$class = [Bugzilla::Classification->get_all];
$class = [ Bugzilla::Classification->get_all ];
}
else
{
@ -84,7 +85,7 @@ if ($useclassification && !$classification_name && !$product_name)
# which you can administer.
my $products = $user->get_editable_products;
my %class_ids = map { $_->classification_id => 1 } @$products;
$class = Bugzilla::Classification->new_from_list([keys %class_ids]);
$class = Bugzilla::Classification->new_from_list([ keys %class_ids ]);
}
$vars->{classifications} = $class;
@ -130,22 +131,21 @@ if (!$action && !$product_name)
if ($action eq 'add')
{
# The user must have the global editcomponents privs to add
# new products.
$user->in_group('editcomponents') || ThrowUserError('auth_failure', {
group => 'editcomponents',
action => 'add',
object => 'products',
});
# The user must have the createproducts or global editcomponents privs to add new products.
$user->in_group('editcomponents') ||
$user->in_group('createproducts') ||
ThrowUserError('auth_failure', {
group => 'editcomponents',
action => 'add',
object => 'products',
});
if ($useclassification)
{
my $classification = Bugzilla::Classification->check($classification_name);
$vars->{classification} = $classification;
$vars->{classification} = $classification_name ? Bugzilla::Classification->new({ name => $classification_name }) : undef;
$vars->{classifications} = [ Bugzilla::Classification->get_all ];
}
$vars->{token} = issue_session_token('add_product');
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
$vars->{all_groups} = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
$template->process('admin/products/create.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
@ -159,13 +159,14 @@ if ($action eq 'add')
if ($action eq 'new')
{
# The user must have the global editcomponents privs to add
# new products.
$user->in_group('editcomponents') || ThrowUserError('auth_failure', {
group => 'editcomponents',
action => 'add',
object => 'products',
});
# The user must have the createproducts or global editcomponents privs to add new products.
$user->in_group('editcomponents') ||
$user->in_group('createproducts') ||
ThrowUserError('auth_failure', {
group => 'editcomponents',
action => 'add',
object => 'products',
});
check_token_data($token, 'add_product');
@ -188,19 +189,31 @@ if ($action eq 'new')
$create_params{maxvotesperbug} = $ARGS->{maxvotesperbug};
$create_params{votestoconfirm} = $ARGS->{votestoconfirm};
}
Bugzilla->dbh->bz_start_transaction();
my $product = Bugzilla::Product->create(\%create_params);
# Create groups and series for the new product, if requested.
$product->_create_bug_group() if $ARGS->{makeproductgroup};
my $new_bug_group = $product->_create_bug_group() if $ARGS->{makeproductgroup};
if (!$user->in_group('editcomponents') || $ARGS->{makeadmingroup})
{
# User has no global editcomponents permission, so grant 'createproducts'
# group the right to manage his newly created product (or admin group is explicitly requested)
$product->_create_bug_group(1, $new_bug_group);
}
$product->_create_series() if $ARGS->{createseries};
$product->check_open_product();
delete_token($token);
Bugzilla->dbh->bz_commit_transaction();
$vars->{message} = 'product_created';
$vars->{product} = $product;
if ($useclassification)
{
$vars->{classification} = new Bugzilla::Classification($product->classification_id);
$vars->{classifications} = [ Bugzilla::Classification->get_all ];
}
$vars->{token} = issue_session_token('edit_product');
@ -285,7 +298,7 @@ if ($action eq 'edit' || (!$action && $product_name))
}
$vars->{product} = $product;
$vars->{token} = issue_session_token('edit_product');
$vars->{all_groups} = [ Bugzilla::Group->get_all ];
$vars->{all_groups} = Bugzilla::Group->match({isactive => 1, isbuggroup => 1});
$template->process('admin/products/edit.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
@ -366,8 +379,9 @@ if ($action eq 'updategroupcontrols')
my $product = $user->check_can_admin_product($product_name);
check_token_data($token, 'edit_group_controls');
my @now_na = ();
my @now_mandatory = ();
my @now_na;
my @now_mandatory;
my @now_entry;
my %membercontrol_g;
my %othercontrol_g;
foreach my $f (keys %$ARGS)
@ -459,6 +473,9 @@ if ($action eq 'updategroupcontrols')
$vars->{product} = $product;
$vars->{changes} = $changes;
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
$template->process('admin/products/groupcontrol/updated.html.tmpl', $vars)
|| ThrowTemplateError($template->error());
exit;

View File

@ -279,22 +279,12 @@ elsif ($action eq 'update')
$changes = $otherUser->update();
}
# Update group settings.
my $sth_add_mapping = $dbh->prepare(
"INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) VALUES (?, ?, ?, ?)"
);
my $sth_remove_mapping = $dbh->prepare(
"DELETE FROM user_group_map WHERE user_id=? AND group_id=? AND isbless=? AND grant_type=?"
);
my @groupsAddedTo;
my @groupsRemovedFrom;
my @groupsGrantedRightsToBless;
my @groupsDeniedRightsToBless;
# Regard only groups the user is allowed to bless and skip all others silently.
# FIXME: checking for existence of each user_group_map entry would allow to
# display a friendlier error message on page reloads.
userDataToVars($otherUserID);
my $permissions = $vars->{permissions};
foreach my $blessable (@{$user->bless_groups()})
@ -308,13 +298,11 @@ elsif ($action eq 'update')
{
if (!$groupid)
{
$sth_remove_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT);
push @groupsRemovedFrom, $name;
push @groupsRemovedFrom, $blessable;
}
else
{
$sth_add_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT);
push @groupsAddedTo, $name;
push @groupsAddedTo, $blessable;
}
}
@ -327,27 +315,20 @@ elsif ($action eq 'update')
{
if (!$groupid)
{
$sth_remove_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT);
push @groupsDeniedRightsToBless, $name;
push @groupsDeniedRightsToBless, $blessable;
}
else
{
$sth_add_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT);
push @groupsGrantedRightsToBless, $name;
push @groupsGrantedRightsToBless, $blessable;
}
}
}
}
if (@groupsAddedTo || @groupsRemovedFrom)
{
$dbh->do(
"INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, oldvalue, newvalue)".
" VALUES (?, ?, NOW(), ?, ?, ?)", undef,
$otherUserID, $userid, Bugzilla->get_field('bug_group')->id,
join(', ', @groupsRemovedFrom), join(', ', @groupsAddedTo)
);
}
# FIXME: should create profiles_activity entries for blesser changes.
Bugzilla::Group::add_user_groups([ map { { group => $_, user => $otherUser } } @groupsAddedTo ]);
Bugzilla::Group::remove_user_groups([ map { { group => $_, user => $otherUser } } @groupsRemovedFrom ]);
Bugzilla::Group::add_user_groups([ map { { group => $_, user => $otherUser } } @groupsGrantedRightsToBless ], 1);
Bugzilla::Group::remove_user_groups([ map { { group => $_, user => $otherUser } } @groupsDeniedRightsToBless ], 1);
$dbh->bz_commit_transaction();
@ -357,13 +338,15 @@ elsif ($action eq 'update')
Bugzilla::Hook::process('editusers-post_update', { userid => $otherUserID });
Bugzilla::Views::refresh_some_views([ $otherUser->login ]);
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
$vars->{message} = 'account_updated';
$vars->{changed_fields} = [keys %$changes];
$vars->{groups_added_to} = \@groupsAddedTo;
$vars->{groups_removed_from} = \@groupsRemovedFrom;
$vars->{groups_granted_rights_to_bless} = \@groupsGrantedRightsToBless;
$vars->{groups_denied_rights_to_bless} = \@groupsDeniedRightsToBless;
$vars->{changed_fields} = [ keys %$changes ];
$vars->{groups_added_to} = [ map { $_->name } @groupsAddedTo ];
$vars->{groups_removed_from} = [ map { $_->name } @groupsRemovedFrom ];
$vars->{groups_granted_rights_to_bless} = [ map { $_->name } @groupsGrantedRightsToBless ];
$vars->{groups_denied_rights_to_bless} = [ map { $_->name } @groupsDeniedRightsToBless ];
# We already display the updated page. We have to recreate a token now.
$vars->{token} = issue_session_token('edit_user');

View File

@ -54,30 +54,14 @@ if (@add_members || @add_bless || @rm_members || @rm_bless)
} };
for (\@add_members, \@add_bless)
{
@$_ = map { $users->{lc $_} ? $users->{lc $_}->id : ThrowUserError('invalid_username', { name => $_ }) } @$_;
}
if (@add_members || @add_bless)
{
# FIXME Use object method instead of direct DB query
Bugzilla->dbh->do(
"INSERT IGNORE INTO user_group_map (user_id, group_id, grant_type, isbless) VALUES ".
join(', ', ("(?, ?, ?, ?)") x (@add_members + @add_bless)), undef,
(map { $_, $vars->{group}->id, GRANT_DIRECT, 0 } @add_members),
(map { $_, $vars->{group}->id, GRANT_DIRECT, 1 } @add_bless)
);
@$_ = map { $users->{lc $_} || ThrowUserError('invalid_username', { name => $_ }) } @$_;
}
$vars->{group}->add_users(\@add_members, 0);
$vars->{group}->add_users(\@add_bless, 1);
}
if (@rm_members || @rm_bless)
{
# FIXME Use object method instead of direct DB query
trick_taint($_) for @rm_members, @rm_bless;
Bugzilla->dbh->do(
"DELETE FROM user_group_map WHERE group_id=? AND grant_type=? AND (user_id, isbless) IN (".
join(', ', ("(?, ?)") x (@rm_members + @rm_bless)).")", undef,
$vars->{group}->id, GRANT_DIRECT,
(map { int($_), 0 } @rm_members), (map { int($_), 1 } @rm_bless)
);
}
trick_taint($_) for @rm_members, @rm_bless;
$vars->{group}->remove_users(\@rm_members, 0);
$vars->{group}->remove_users(\@rm_bless, 1);
if (@add_members || @rm_members)
{
Bugzilla::Hook::process('editusersingroup-post_add', {
@ -89,6 +73,8 @@ if (@add_members || @add_bless || @rm_members || @rm_bless)
if (@add_members || @rm_members)
{
Bugzilla::Views::refresh_some_views();
# Refresh fieldvaluecontrol cache
Bugzilla->get_field('delta_ts')->touch;
}
delete_token($ARGS->{token});
my $url = "editusersingroup.cgi?group=".$vars->{group}->id;

View File

@ -165,7 +165,7 @@ if ($action eq 'update')
}
if ($value->field->value_field)
{
$vars->{changes}->{visibility_values} = $value->set_visibility_values([ list $ARGS->{visibility_value_id} ]);
$vars->{changes}->{visibility_values} = $value->set_visibility_values([ list $ARGS->{visibility_value_id} ], 'SKIP_INVISIBLE');
}
$vars->{changes}->{control_lists} = 1 if $field->update_control_lists($value->id, $ARGS);
delete_token($token);

View File

@ -247,6 +247,7 @@ if ($ARGS->{update})
my $title = $ARGS->{"query_title_$qid"} || '';
my $o_onemailperbug = $ARGS->{"orig_query_onemailperbug_$qid"} || 0;
my $onemailperbug = $ARGS->{"query_onemailperbug_$qid"} ? 1 : 0;
my $isreport = 0;
if ($o_sort != $sort || $o_queryname ne $queryname ||
$o_onemailperbug != $onemailperbug || $o_title ne $title)
@ -254,9 +255,13 @@ if ($ARGS->{update})
detaint_natural($sort);
trick_taint($queryname);
trick_taint($title);
if ($queryname =~ /^([01])-(.*)$/s)
{
($isreport, $queryname) = ($1, $2);
}
$dbh->do(
"UPDATE whine_queries SET sortkey=?, query_name=?, title=?, onemailperbug=? WHERE id=?",
undef, $sort, $queryname, $title, $onemailperbug, $qid
"UPDATE whine_queries SET sortkey=?, query_name=?, title=?, onemailperbug=?, isreport=? WHERE id=?",
undef, $sort, $queryname, $title, $onemailperbug, $isreport, $qid
);
}
}
@ -298,11 +303,11 @@ for my $event_id (keys %{$events})
my $mailto = '';
if ($mailto_type == MAILTO_USER)
{
$mailto = $schedule->mailto->login;
$mailto = $schedule->mailto && $schedule->mailto->login;
}
elsif ($mailto_type == MAILTO_GROUP)
{
$mailto = $schedule->mailto->name;
$mailto = $schedule->mailto && $schedule->mailto->name;
}
push @{$events->{$event_id}->{schedule}}, {
day => $schedule->run_day,
@ -323,6 +328,7 @@ for my $event_id (keys %{$events})
sort => $query->sortkey,
id => $query->id,
onemailperbug => $query->one_email_per_bug,
isreport => $query->isreport,
};
}
}
@ -330,7 +336,8 @@ for my $event_id (keys %{$events})
$vars->{events} = $events;
# get the available queries
$vars->{available_queries} = $dbh->selectcol_arrayref("SELECT name FROM namedqueries WHERE userid=?", undef, $userid) || [];
$vars->{available_queries} = $dbh->selectcol_arrayref("SELECT name FROM namedqueries WHERE userid=? ORDER BY name", undef, $userid) || [];
$vars->{available_reports} = $dbh->selectcol_arrayref("SELECT name FROM reports WHERE user_id=? ORDER BY name", undef, $userid) || [];
$vars->{token} = issue_session_token('edit_whine');
$vars->{local_timezone} = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now());

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

@ -25,13 +25,10 @@ use strict;
use Bugzilla::Extension;
my $VERSION = '1.0';
my $REQUIRED_MODULES = [
{
package => 'PerlMagick',
module => 'Image::Magick',
version => 0,
},
];
my $REQUIRED_MODULES = [ {
package => 'PerlMagick',
module => 'Image::Magick',
} ];
extension_version('BmpConvert', $VERSION);
required_modules('BmpConvert', $REQUIRED_MODULES);

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;
}
@ -493,10 +514,20 @@ function to_attachment_page(link)
var form = document.createElement('form');
form.action = link.href;
form.method = 'post';
var textarea = document.createElement('textarea');
textarea.name = "comment";
textarea.value = document.getElementById('comment_textarea').value;
form.appendChild(textarea);
var e = document.createElement('input');
e.type = 'hidden';
e.name = 'comment';
e.value = document.getElementById('comment_textarea').value;
form.appendChild(e);
var w = document.getElementById('work_time');
if (w)
{
e = document.createElement('input');
e.type = 'hidden';
e.name = 'work_time';
e.value = w.value;
form.appendChild(e);
}
document.body.appendChild(form);
form.submit();
return false;

View File

@ -144,7 +144,7 @@ function userAutocomplete(hint, emptyOptions, loadAllOnEmpty)
}
var u = window.location.href.replace(/[^\/]+$/, '');
u += 'xml.cgi?method=User.get&output=json&excludedisabled=1&maxusermatches=';
u += 'xml.cgi?method=User.get&output=json&excludedisabled=1&include_fields=id&include_fields=real_name&include_fields=email&maxusermatches=';
if (hint.input.value)
{
u += '20';

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

@ -221,7 +221,7 @@ if ($ARGS->{delta_ts})
$vars->{title_tag} = "mid_air";
ThrowCodeError('undefined_field', { field => 'longdesclength' }) if !$ARGS->{longdesclength};
ThrowCodeError('undefined_field', { field => 'longdesclength' }) if !defined $ARGS->{longdesclength};
$vars->{start_at} = $ARGS->{longdesclength};
# Always sort midair collision comments oldest to newest,

View File

@ -179,7 +179,7 @@ $vars->{chfield} = [
# Fields for reports
$vars->{report_columns} = [
sort { $a->{sortkey} <=> $b->{sortkey} || $a->{title} cmp $b->{title} }
values %{Bugzilla::Search::REPORT_COLUMNS()}
values %{Bugzilla::Search->REPORT_COLUMNS()}
];
# Boolean charts

View File

@ -34,7 +34,6 @@ use Bugzilla::Token;
my $ARGS = Bugzilla->input_params;
my $template = Bugzilla->template;
my $vars = {};
my $buffer = http_build_query($ARGS);
# Go straight back to query.cgi if we are adding a boolean chart.
if (grep /^cmd-/, keys %$ARGS)
@ -52,7 +51,8 @@ my $dbh = Bugzilla->switch_to_shadow_db();
my $action = $ARGS->{action} || 'menu';
my $token = $ARGS->{token};
if ($action eq "menu")
$vars->{measure_descs} = Bugzilla::Report->get_measures();
if ($action eq 'menu')
{
# No need to do any searching in this case, so bail out early.
$template->process("reports/menu.html.tmpl", $vars)
@ -104,210 +104,17 @@ elsif ($action eq 'del')
exit;
}
my $valid_columns = Bugzilla::Search::REPORT_COLUMNS();
$vars->{report_columns} = $valid_columns;
my $field = {};
for (qw(x y z))
{
my $f = $ARGS->{$_.'_axis_field'} || '';
trick_taint($f);
if ($f)
{
if ($valid_columns->{$f})
{
$field->{$_} = $f;
}
else
{
ThrowCodeError("report_axis_invalid", {fld => $_, val => $f});
}
}
}
if (!keys %$field)
{
ThrowUserError("no_axes_defined");
}
my $width = $ARGS->{width};
my $height = $ARGS->{height};
if (defined($width))
{
(detaint_natural($width) && $width > 0)
|| ThrowCodeError("invalid_dimensions");
$width <= 2000 || ThrowUserError("chart_too_large");
}
if (defined($height))
{
(detaint_natural($height) && $height > 0)
|| ThrowCodeError("invalid_dimensions");
$height <= 2000 || ThrowUserError("chart_too_large");
}
# These shenanigans are necessary to make sure that both vertical and
# horizontal 1D tables convert to the correct dimension when you ask to
# display them as some sort of chart.
my $is_table;
if ($ARGS->{format} eq 'table' || $ARGS->{format} eq 'simple')
{
$is_table = 1;
if ($field->{x} && !$field->{y})
{
# 1D *tables* should be displayed vertically (with a row_field only)
$field->{y} = $field->{x};
delete $field->{x};
}
}
else
{
if (!Bugzilla->feature('graphical_reports'))
{
ThrowCodeError('feature_disabled', { feature => 'graphical_reports' });
}
if ($field->{y} && !$field->{x})
{
# 1D *charts* should be displayed horizontally (with an col_field only)
$field->{x} = $field->{y};
delete $field->{y};
}
}
my $measures = {
etime => 'estimated_time',
rtime => 'remaining_time',
wtime => 'interval_time',
count => '_count',
};
# Trick Bugzilla::Search: replace report columns SQL + add '_count' column
# FIXME: Remove usage of global variable COLUMNS in search generation code
%{Bugzilla::Search->COLUMNS} = (%{Bugzilla::Search->COLUMNS}, %{Bugzilla::Search->REPORT_COLUMNS});
Bugzilla::Search->COLUMNS->{_count}->{name} = '1';
my $measure = $ARGS->{measure};
if ($measure eq 'times' ? !$is_table : !$measures->{$measure})
{
$measure = 'count';
}
$vars->{measure} = $measure;
# Validate the values in the axis fields or throw an error.
my %a;
my @group_by = grep { !($a{$_}++) } values %$field;
my @axis_fields = @group_by;
for ($measure eq 'times' ? qw(etime rtime wtime) : $measure)
{
push @axis_fields, $measures->{$_} unless $a{$measures->{$_}};
}
# Clone the params, so that Bugzilla::Search can modify them
my $search = new Bugzilla::Search(
'fields' => \@axis_fields,
'params' => { %{ Bugzilla->input_params } },
);
my $query = $search->getSQL();
$query =
"SELECT ".
($field->{x} || "''")." x, ".
($field->{y} || "''")." y, ".
($field->{z} || "''")." z, ".
join(', ', map { "SUM($measures->{$_}) $_" } $measure eq 'times' ? qw(etime rtime wtime) : $measure).
" FROM ($query) _report_table GROUP BY ".join(", ", @group_by);
$::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';
my $results = $dbh->selectall_arrayref($query, {Slice=>{}});
# We have a hash of hashes for the data itself, and a hash to hold the
# row/col/table names.
my %data;
my %names;
# Read the bug data and count the bugs for each possible value of row, column
# and table.
#
# We detect a numerical field, and sort appropriately, if all the values are
# numeric.
my %isnumeric;
foreach my $group (@$results)
{
for (qw(x y z))
{
$isnumeric{$_} &&= ($group->{$_} =~ /^-?\d+(\.\d+)?$/o);
$names{$_}{$group->{$_}} = 1;
}
$data{$group->{z}}{$group->{x}}{$group->{y}} = $is_table ? $group : $group->{$measure};
}
my @tbl_names = @{get_names($names{z}, $isnumeric{z}, $field->{z})};
my @col_names = @{get_names($names{x}, $isnumeric{x}, $field->{x})};
my @row_names = @{get_names($names{y}, $isnumeric{y}, $field->{y})};
# The GD::Graph package requires a particular format of data, so once we've
# gathered everything into the hashes and made sure we know the size of the
# data, we reformat it into an array of arrays of arrays of data.
push @tbl_names, "-total-" if scalar(@tbl_names) > 1;
my @image_data;
foreach my $tbl (@tbl_names)
{
my @tbl_data;
push @tbl_data, \@col_names;
foreach my $row (@row_names)
{
my @col_data;
foreach my $col (@col_names)
{
$data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0;
push @col_data, $data{$tbl}{$col}{$row};
if ($tbl ne "-total-")
{
# This is a bit sneaky. We spend every loop except the last
# building up the -total- data, and then last time round,
# we process it as another tbl, and push() the total values
# into the image_data array.
$data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row};
}
}
push @tbl_data, \@col_data;
}
unshift @image_data, \@tbl_data;
}
$vars->{tbl_field} = $field->{z};
$vars->{col_field} = $field->{x};
$vars->{row_field} = $field->{y};
$vars->{time} = localtime(time());
$vars->{col_names} = \@col_names;
$vars->{row_names} = \@row_names;
$vars->{tbl_names} = \@tbl_names;
# Below a certain width, we don't see any bars, so there needs to be a minimum.
if ($width && $ARGS->{format} eq "bar")
{
my $min_width = (scalar(@col_names) || 1) * 20;
if (!$ARGS->{cumulate})
{
$min_width *= (scalar(@row_names) || 1);
}
$vars->{min_width} = $min_width;
}
$vars->{width} = $width if $width;
$vars->{height} = $height if $height;
$vars->{query} = $query;
$vars = { %$vars, %{Bugzilla::Report->execute($ARGS)} };
$vars->{saved_report_id} = $ARGS->{saved_report_id};
$vars->{debug} = $ARGS->{debug};
$vars->{report_columns} = Bugzilla::Search->REPORT_COLUMNS();
$ARGS->{ctype} = $ARGS->{action} eq 'plot' ? 'png' : ($ARGS->{format} eq 'csv' ? 'csv' : 'html');
$ARGS->{format} = $ARGS->{format} eq 'csv' ? 'table' : $ARGS->{format};
my $formatparam = $ARGS->{format};
if ($action eq "wrap")
if ($action eq 'wrap')
{
# So which template are we using? If action is "wrap", we will be using
# no format (it gets passed through to be the format of the actual data),
@ -320,32 +127,22 @@ if ($action eq "wrap")
$vars->{format} = $formatparam;
$formatparam = '' if $formatparam ne 'simple';
# We need to keep track of the defined restrictions on each of the
# axes, because buglistbase, below, throws them away. Without this, we
# get buglistlinks wrong if there is a restriction on an axis field.
$vars->{col_vals} = join("&", $buffer =~ /[&?]($field->{x}=[^&]+)/g);
$vars->{row_vals} = join("&", $buffer =~ /[&?]($field->{y}=[^&]+)/g);
$vars->{tbl_vals} = join("&", $buffer =~ /[&?]($field->{z}=[^&]+)/g);
# We need a number of different variants of the base URL for different URLs in the HTML.
my $a = { %$ARGS };
delete $a->{$_} for qw(x_axis_field y_axis_field z_axis_field ctype format query_format measure), @axis_fields;
$vars->{buglistbase} = http_build_query($a);
$a = { %$ARGS };
delete $a->{$_} for $field->{z}, qw(action ctype format width height);
delete $a->{$_} for $vars->{fields}->{z}, qw(action ctype format width height);
$vars->{imagebase} = http_build_query($a);
$a = { %$ARGS };
delete $a->{$_} for qw(query_format action ctype format width height measure);
for (keys %$a)
{
delete $a->{$_} if $a->{$_} eq '';
}
$vars->{switchparams} = $a;
$vars->{switchbase} = http_build_query($a);
$vars->{data} = \%data;
}
elsif ($action eq "plot")
{
# If action is "plot", we will be using a format as normal (pie, bar etc.)
# and a ctype as normal (currently only png.)
$vars->{cumulate} = $ARGS->{cumulate} ? 1 : 0;
$vars->{x_labels_vertical} = $ARGS->{x_labels_vertical} ? 1 : 0;
$vars->{data} = \@image_data;
}
else
{
@ -373,9 +170,9 @@ if ($ARGS->{debug})
{
require Data::Dumper;
print "<pre>data hash:\n";
print html_quote(Data::Dumper::Dumper(%data)) . "\n\n";
print html_quote(Data::Dumper::Dumper($vars->{data})) . "\n\n";
print "data array:\n";
print html_quote(Data::Dumper::Dumper(@image_data)) . "\n\n</pre>";
print html_quote(Data::Dumper::Dumper($vars->{image_data})) . "\n\n</pre>";
}
# All formats point to the same section of the documentation.
@ -387,30 +184,3 @@ $template->process($format->{template}, $vars)
|| ThrowTemplateError($template->error());
exit;
sub get_names
{
my ($names, $isnumeric, $field) = @_;
# These are all the fields we want to preserve the order of in reports.
my $f = Bugzilla->get_field($field);
if ($f && $f->is_select)
{
my $values = [ '', map { $_->name } @{ $f->legal_values(1) } ];
my %dup;
@$values = grep { exists($names->{$_}) && !($dup{$_}++) } @$values;
return $values;
}
elsif ($isnumeric)
{
# It's not a field we are preserving the order of, so sort it
# numerically...
sub numerically { $a <=> $b }
return [ sort numerically keys %$names ];
}
else
{
# ...or alphabetically, as appropriate.
return [ sort keys %$names ];
}
}

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

@ -156,6 +156,8 @@ $vars->{comment_indexes} = sub
return [ map { [ $_->{count}, $_->{comment_id}, $_->{type} != CMT_WORKTIME && $_->{type} != CMT_BACKDATED_WORKTIME ? 1 : 0 ] } @$comments ];
};
$vars->{look_in_urls} = [ map { [ split /:/, $_, 2 ] } grep { /^(?!\s*(#|$))/so } split /\n/, Bugzilla->params->{look_in_urls} ];
Bugzilla->cgi->send_header($format->{ctype});
$template->process($format->{template}, $vars)
|| ThrowTemplateError($template->error());

View File

@ -946,13 +946,16 @@ table.tabs {
}
table.eventbox {
border-color: gray;
padding: 5px;
border-color: white;
background: white;
border-radius: 5px;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
}
table.eventbox td.set {
background: #e0e0e0;
}
/* search */
#summary_field.search_field_row input {

View File

@ -95,16 +95,16 @@ td.forbidden {
}
table.eventbox {
border: 1px solid black;
border: 10px solid white;
margin: 8px 0;
}
td.set {
table.eventbox td.set {
background: #ddd;
padding: 5px;
}
td.unset {
table.eventbox td.unset {
color: red;
}

View File

@ -24,10 +24,10 @@
text-decoration: none;
}
th.sorttable_sorted a:after { content: "▲"; color: gray; }
th.sorttable_sorted_reverse a:after { content: "▼"; color: gray; }
th.sorted_0.sorttable_sorted a:after { content: "▲"; color: black; }
th.sorted_0.sorttable_sorted_reverse a:after { content: "▼"; color: black; }
th.sorttable_sorted a:after { content: "\25B2";/*▲*/ color: gray; }
th.sorttable_sorted_reverse a:after { content: "\25BC";/*▼*/ color: gray; }
th.sorted_0.sorttable_sorted a:after { content: "\25B2";/*▲*/ color: black; }
th.sorted_0.sorttable_sorted_reverse a:after { content: "\25BC";/*▼*/ color: black; }
th.sorted_0 { background-color: #a0a0a0; }
th.sorted_1 { background-color: #acacac; }
th.sorted_2 { background-color: #b8b8b8; }
@ -78,7 +78,8 @@ td.bz_total {
.bz_buglist .bz_estimated_time_column,
.bz_buglist .bz_remaining_time_column,
.bz_buglist .bz_work_time_column,
.bz_buglist .bz_percentage_complete_column { text-align: right; }
.bz_buglist .bz_percentage_complete_column,
.bz_buglist .bz_f30 { text-align: right; }
.bz_buglist .bz_short_desc_column,
.bz_buglist .bz_short_short_desc_column,
@ -87,7 +88,8 @@ td.bz_total {
.bz_buglist .bz_flagtypes_name_column,
.bz_buglist .bz_f1,
.bz_buglist .bz_f4,
.bz_buglist .bz_f7 { white-space: normal; }
.bz_buglist .bz_f7,
.bz_buglist .bz_customfield { white-space: normal; }
.bz_buglist .bz_comment0_column,
.bz_buglist .bz_lastcomment_column { width: 15%; white-space: normal; }

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

@ -59,3 +59,11 @@ dd {
padding: .5em;
font-size: small;
}
pre.cfg_example {
margin: 8px 0;
padding: 4px;
background: white;
float: left;
border: 1px solid gray;
}

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

@ -107,7 +107,7 @@ my $i = 0;
for my $q (@$queries)
{
my $key = $q->{userid}.':'.$q->{name};
my $user = Bugzilla->request_cache->{user} = Bugzilla::User->new({ id => $q->{userid} });
my $user = Bugzilla::User->new({ id => $q->{userid} });
print("Invalid user $q->{userid}!\n"), next unless $user;
next if $user->disabledtext;
my $s = "Testing $q->{userid}'s $q->{name}... ";

View File

@ -39,7 +39,7 @@
<label for="Bugzilla_restrictlogin" title="Restrict this session to this IP address (using this option improves security)">Restrict to IP</label>
[% PROCESS "global/hidden-fields.html.tmpl"
exclude="^Bugzilla_(login|password|restrictlogin)$" %]
exclude="^Bugzilla_(login|password|restrictlogin)$|^logout$|^GoAheadAndLogIn$" %]
<input type="submit" name="GoAheadAndLogIn" value="Log in to [% terms.Bugzilla %]" id="log_in" />

View File

@ -41,9 +41,10 @@
[%# Use the current script name. If an empty name is returned,
# then we are accessing the home page. %]
[% cgi.delete('logout') %]
[% login_target = cgi.url("-relative" => 1, "-query" => 1) %]
[% IF !login_target OR login_target.match("^token.cgi") %]
[% login_target = "index.cgi" %]
[% login_target = "index.cgi" %]
[% END %]
[% login_target = urlbase _ login_target %]

View File

@ -77,7 +77,7 @@
</table>
[% PROCESS "global/hidden-fields.html.tmpl"
exclude="^Bugzilla_(login|password|restrictlogin)$" %]
exclude="^Bugzilla_(login|password|restrictlogin)$|^logout$|^GoAheadAndLogIn$" %]
<input type="submit" name="GoAheadAndLogIn" value="Log in" id="log_in" />

View File

@ -92,7 +92,7 @@
<option value="[% q.id %]" [% " selected='selected'" IF checker.query_id == q.id %] >[% q.name | html %]</option>
[% END %]
[% IF checker.query_id AND !found %]
<option value="[% checker.query_id %]" selected='selected'>(Чужой) [% checker.query.name | html %]</option>
<option value="[% checker.query_id %]" selected='selected'>([% checker.query.user.login | html %]) [% checker.query.name | html %]</option>
[% END %]
</select></td>
</tr>
@ -128,6 +128,18 @@
<label for="is_fatal">Жёсткий запрет (если нет, то изменения не блокируются, а только даётся предупреждение)</label><br />
</td>
</tr>
<tr><th style="text-align: left" colspan="2">Разрешить следующим пользователям обходить эту проверку:</th></tr>
<tr>
<th>Группа:</th>
<td>
<select name="bypass_group_id" id="bypass_group_id">
<option value="">&mdash; (не разрешать)</option>
[% FOR g = all_groups %]
<option value="[% g.id %]" [% " selected=\"selected\"" IF checker.bypass_group_id == g.id %]>[% g.name | html %]</option>
[% END %]
</select>
</td>
</tr>
<tr><th style="text-align: left" colspan="2">Запреты изменений отдельных полей, действуют только при обновлении багов:</th></tr>
<tr>
<th style="background: #FFE0E0">Запрещать:</th>
@ -222,7 +234,12 @@ function check_trigger()
showhide_allowdeny();
[% IF checker.except_fields %]
[% FOR f = checker.except_fields.keys %]
add_field("[% f | js %]", "[% checker.except_fields.$f | js %]");
[% IF !checker.except_fields.$f %]
add_field("[% f | js %]", "");
[% END %]
[% FOR v = checker.except_fields.$f %]
add_field("[% f | js %]", "[% v | js %]");
[% END %]
[% END %]
[% ELSE %]
add_field();

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

@ -18,18 +18,15 @@
_ " \$EMAIL will be replaced by cleartext user email, so you should only never use it in public networks;"
_ " \$MD5 will be replaced by MD5 hash of user email, just like it is required by real Gravatar service."
_ " You can also disable avatar display by clearing this parameter.",
viewvc_url =>
"ViewVC query URL for browsing bug-related commits. Bugzilla4Intranet links to ViewVC search page with full-text query 'bugXXX XXX'"
_ " which allows to show commits with the bug number in the commit message (like 'Bug XXX' or 'BugXXX').",
git_url =>
"Git (GitBlit or other web UI) full-text query URL for browsing bug-related commits. '$q' in URL will be replaced by 'bugXXX XXX'"
_ " which allows to show commits with the bug number in the commit message (like 'Bug XXX' or 'BugXXX').",
look_in_urls =>
"<p style='margin: 0'>VCS/Wiki/whatever query URLs for 'Look for bug in ...' links, one per line, separated by ':'.</p>" _
"<pre class='cfg_example'>CVS/SVN: http://viewvc.local/?view=query&comment=bug\$BUG+\$BUG&comment_match=fulltext&querysort=date&date=all</pre>" _
"<p style='margin: 0; clear: both'>$BUG will be replaced with bug ID in these URLs.</p>",
wiki_url =>
"Default MediaWiki URL for bug links. Bugzilla4Intranet links to <tt>&lt;wiki_url&gt;/Bug_XXX</tt> pages when this is non-empty.",
mediawiki_urls =>
"<p style='margin: 0'>Known MediaWiki URLs to be quoted in bug comments, one per line. Example:</p>"
_ "<pre style='margin: 8px 0; padding: 4px; background: white; float: left;"
_ " border: 1px solid gray;'>wikipedia http://en.wikipedia.org/wiki/</pre>"
_ "<pre class='cfg_example'>wikipedia http://en.wikipedia.org/wiki/</pre>"
_ "<p style='margin: 0; clear: both'>Links like <b><tt>wikipedia:Article_name#Section</tt></b>"
_ " and <b><tt>wikipedia:[[Article name#Section]]</tt></b><br /> will be quoted"
_ " and lead to <b>Section</b> (optional) of <b>Article name</b> page in the Wikipedia.</p>",

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

Some files were not shown because too many files have changed in this diff Show More