Flag.pm 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194
  1. # -*- Mode: perl; indent-tabs-mode: nil -*-
  2. #
  3. # The contents of this file are subject to the Mozilla Public
  4. # License Version 1.1 (the "License"); you may not use this file
  5. # except in compliance with the License. You may obtain a copy of
  6. # the License at http://www.mozilla.org/MPL/
  7. #
  8. # Software distributed under the License is distributed on an "AS
  9. # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
  10. # implied. See the License for the specific language governing
  11. # rights and limitations under the License.
  12. #
  13. # The Original Code is the Bugzilla Bug Tracking System.
  14. #
  15. # The Initial Developer of the Original Code is Netscape Communications
  16. # Corporation. Portions created by Netscape are
  17. # Copyright (C) 1998 Netscape Communications Corporation. All
  18. # Rights Reserved.
  19. #
  20. # Contributor(s): Myk Melez <myk@mozilla.org>
  21. # Jouni Heikniemi <jouni@heikniemi.net>
  22. # Frédéric Buclin <LpSolit@gmail.com>
  23. use strict;
  24. package Bugzilla::Flag;
  25. =head1 NAME
  26. Bugzilla::Flag - A module to deal with Bugzilla flag values.
  27. =head1 SYNOPSIS
  28. Flag.pm provides an interface to flags as stored in Bugzilla.
  29. See below for more information.
  30. =head1 NOTES
  31. =over
  32. =item *
  33. Import relevant functions from that script.
  34. =item *
  35. Use of private functions / variables outside this module may lead to
  36. unexpected results after an upgrade. Please avoid using private
  37. functions in other files/modules. Private functions are functions
  38. whose names start with _ or a re specifically noted as being private.
  39. =back
  40. =cut
  41. use Bugzilla::FlagType;
  42. use Bugzilla::Hook;
  43. use Bugzilla::User;
  44. use Bugzilla::Util;
  45. use Bugzilla::Error;
  46. use Bugzilla::Mailer;
  47. use Bugzilla::Constants;
  48. use Bugzilla::Field;
  49. use base qw(Bugzilla::Object Exporter);
  50. @Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
  51. ###############################
  52. #### Initialization ####
  53. ###############################
  54. use constant DB_COLUMNS => qw(
  55. flags.id
  56. flags.type_id
  57. flags.bug_id
  58. flags.attach_id
  59. flags.requestee_id
  60. flags.setter_id
  61. flags.status
  62. );
  63. use constant DB_TABLE => 'flags';
  64. use constant LIST_ORDER => 'id';
  65. use constant SKIP_REQUESTEE_ON_ERROR => 1;
  66. ###############################
  67. #### Accessors ######
  68. ###############################
  69. =head2 METHODS
  70. =over
  71. =item C<id>
  72. Returns the ID of the flag.
  73. =item C<name>
  74. Returns the name of the flagtype the flag belongs to.
  75. =item C<bug_id>
  76. Returns the ID of the bug this flag belongs to.
  77. =item C<attach_id>
  78. Returns the ID of the attachment this flag belongs to, if any.
  79. =item C<status>
  80. Returns the status '+', '-', '?' of the flag.
  81. =back
  82. =cut
  83. sub id { return $_[0]->{'id'}; }
  84. sub name { return $_[0]->type->name; }
  85. sub bug_id { return $_[0]->{'bug_id'}; }
  86. sub attach_id { return $_[0]->{'attach_id'}; }
  87. sub status { return $_[0]->{'status'}; }
  88. ###############################
  89. #### Methods ####
  90. ###############################
  91. =pod
  92. =over
  93. =item C<type>
  94. Returns the type of the flag, as a Bugzilla::FlagType object.
  95. =item C<setter>
  96. Returns the user who set the flag, as a Bugzilla::User object.
  97. =item C<requestee>
  98. Returns the user who has been requested to set the flag, as a
  99. Bugzilla::User object.
  100. =item C<attachment>
  101. Returns the attachment object the flag belongs to if the flag
  102. is an attachment flag, else undefined.
  103. =back
  104. =cut
  105. sub type {
  106. my $self = shift;
  107. $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
  108. return $self->{'type'};
  109. }
  110. sub setter {
  111. my $self = shift;
  112. $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
  113. return $self->{'setter'};
  114. }
  115. sub requestee {
  116. my $self = shift;
  117. if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
  118. $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
  119. }
  120. return $self->{'requestee'};
  121. }
  122. sub attachment {
  123. my $self = shift;
  124. return undef unless $self->attach_id;
  125. require Bugzilla::Attachment;
  126. $self->{'attachment'} ||= Bugzilla::Attachment->get($self->attach_id);
  127. return $self->{'attachment'};
  128. }
  129. ################################
  130. ## Searching/Retrieving Flags ##
  131. ################################
  132. =pod
  133. =over
  134. =item C<has_flags>
  135. Returns 1 if at least one flag exists in the DB, else 0. This subroutine
  136. is mainly used to decide to display the "(My )Requests" link in the footer.
  137. =back
  138. =cut
  139. sub has_flags {
  140. my $dbh = Bugzilla->dbh;
  141. my $has_flags = $dbh->selectrow_array('SELECT 1 FROM flags ' . $dbh->sql_limit(1));
  142. return $has_flags || 0;
  143. }
  144. =pod
  145. =over
  146. =item C<match($criteria)>
  147. Queries the database for flags matching the given criteria
  148. (specified as a hash of field names and their matching values)
  149. and returns an array of matching records.
  150. =back
  151. =cut
  152. sub match {
  153. my $class = shift;
  154. my ($criteria) = @_;
  155. # If the caller specified only bug or attachment flags,
  156. # limit the query to those kinds of flags.
  157. if (my $type = delete $criteria->{'target_type'}) {
  158. if ($type eq 'bug') {
  159. $criteria->{'attach_id'} = IS_NULL;
  160. }
  161. elsif (!defined $criteria->{'attach_id'}) {
  162. $criteria->{'attach_id'} = NOT_NULL;
  163. }
  164. }
  165. # Flag->snapshot() calls Flag->match() with bug_id and attach_id
  166. # as hash keys, even if attach_id is undefined.
  167. if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) {
  168. $criteria->{'attach_id'} = IS_NULL;
  169. }
  170. return $class->SUPER::match(@_);
  171. }
  172. =pod
  173. =over
  174. =item C<count($criteria)>
  175. Queries the database for flags matching the given criteria
  176. (specified as a hash of field names and their matching values)
  177. and returns an array of matching records.
  178. =back
  179. =cut
  180. sub count {
  181. my $class = shift;
  182. return scalar @{$class->match(@_)};
  183. }
  184. ######################################################################
  185. # Creating and Modifying
  186. ######################################################################
  187. =pod
  188. =over
  189. =item C<validate($bug_id, $attach_id, $skip_requestee_on_error)>
  190. Validates fields containing flag modifications.
  191. If the attachment is new, it has no ID yet and $attach_id is set
  192. to -1 to force its check anyway.
  193. =back
  194. =cut
  195. sub validate {
  196. my ($bug_id, $attach_id, $skip_requestee_on_error) = @_;
  197. my $cgi = Bugzilla->cgi;
  198. my $dbh = Bugzilla->dbh;
  199. # Get a list of flags to validate. Uses the "map" function
  200. # to extract flag IDs from form field names by matching fields
  201. # whose name looks like "flag_type-nnn" (new flags) or "flag-nnn"
  202. # (existing flags), where "nnn" is the ID, and returning just
  203. # the ID portion of matching field names.
  204. my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
  205. my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
  206. return unless (scalar(@flagtype_ids) || scalar(@flag_ids));
  207. # No flag reference should exist when changing several bugs at once.
  208. ThrowCodeError("flags_not_available", { type => 'b' }) unless $bug_id;
  209. # We don't check that these new flags are valid for this bug/attachment,
  210. # because the bug may be moved into another product meanwhile.
  211. # This check will be done later when creating new flags, see FormToNewFlags().
  212. if (scalar(@flag_ids)) {
  213. # No reference to existing flags should exist when creating a new
  214. # attachment.
  215. if ($attach_id && ($attach_id < 0)) {
  216. ThrowCodeError('flags_not_available', { type => 'a' });
  217. }
  218. # Make sure all existing flags belong to the bug/attachment
  219. # they pretend to be.
  220. my $field = ($attach_id) ? "attach_id" : "bug_id";
  221. my $field_id = $attach_id || $bug_id;
  222. my $not = ($attach_id) ? "" : "NOT";
  223. my $invalid_data =
  224. $dbh->selectrow_array(
  225. "SELECT 1 FROM flags
  226. WHERE "
  227. . $dbh->sql_in('id', \@flag_ids)
  228. . " AND ($field != ? OR attach_id IS $not NULL) "
  229. . $dbh->sql_limit(1), undef, $field_id);
  230. if ($invalid_data) {
  231. ThrowCodeError('invalid_flag_association',
  232. { bug_id => $bug_id,
  233. attach_id => $attach_id });
  234. }
  235. }
  236. # Validate new flags.
  237. foreach my $id (@flagtype_ids) {
  238. my $status = $cgi->param("flag_type-$id");
  239. my @requestees = $cgi->param("requestee_type-$id");
  240. my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
  241. # Don't bother validating types the user didn't touch.
  242. next if $status eq 'X';
  243. # Make sure the flag type exists. If it doesn't, FormToNewFlags()
  244. # will ignore it, so it's safe to ignore it here.
  245. my $flag_type = new Bugzilla::FlagType($id);
  246. next unless $flag_type;
  247. # Make sure the flag type is active.
  248. unless ($flag_type->is_active) {
  249. ThrowCodeError('flag_type_inactive', {'type' => $flag_type->name});
  250. }
  251. _validate(undef, $flag_type, $status, undef, \@requestees, $private_attachment,
  252. $bug_id, $attach_id, $skip_requestee_on_error);
  253. }
  254. # Validate existing flags.
  255. foreach my $id (@flag_ids) {
  256. my $status = $cgi->param("flag-$id");
  257. my @requestees = $cgi->param("requestee-$id");
  258. my $private_attachment = $cgi->param('isprivate') ? 1 : 0;
  259. # Make sure the flag exists. If it doesn't, process() will ignore it,
  260. # so it's safe to ignore it here.
  261. my $flag = new Bugzilla::Flag($id);
  262. next unless $flag;
  263. _validate($flag, $flag->type, $status, undef, \@requestees, $private_attachment,
  264. undef, undef, $skip_requestee_on_error);
  265. }
  266. }
  267. sub _validate {
  268. my ($flag, $flag_type, $status, $setter, $requestees, $private_attachment,
  269. $bug_id, $attach_id, $skip_requestee_on_error) = @_;
  270. # By default, the flag setter (or requester) is the current user.
  271. $setter ||= Bugzilla->user;
  272. my $id = $flag ? $flag->id : $flag_type->id; # Used in the error messages below.
  273. $bug_id ||= $flag->bug_id;
  274. $attach_id ||= $flag->attach_id if $flag; # Maybe it's a bug flag.
  275. # Make sure the user chose a valid status.
  276. grep($status eq $_, qw(X + - ?))
  277. || ThrowCodeError('flag_status_invalid',
  278. { id => $id, status => $status });
  279. # Make sure the user didn't request the flag unless it's requestable.
  280. # If the flag existed and was requested before it became unrequestable,
  281. # leave it as is.
  282. if ($status eq '?'
  283. && (!$flag || $flag->status ne '?')
  284. && !$flag_type->is_requestable)
  285. {
  286. ThrowCodeError('flag_status_invalid',
  287. { id => $id, status => $status });
  288. }
  289. # Make sure the user didn't specify a requestee unless the flag
  290. # is specifically requestable. For existing flags, if the requestee
  291. # was set before the flag became specifically unrequestable, don't
  292. # let the user change the requestee, but let the user remove it by
  293. # entering an empty string for the requestee.
  294. if ($status eq '?' && !$flag_type->is_requesteeble) {
  295. my $old_requestee = ($flag && $flag->requestee) ?
  296. $flag->requestee->login : '';
  297. my $new_requestee = join('', @$requestees);
  298. if ($new_requestee && $new_requestee ne $old_requestee) {
  299. ThrowCodeError('flag_requestee_disabled',
  300. { type => $flag_type });
  301. }
  302. }
  303. # Make sure the user didn't enter multiple requestees for a flag
  304. # that can't be requested from more than one person at a time.
  305. if ($status eq '?'
  306. && !$flag_type->is_multiplicable
  307. && scalar(@$requestees) > 1)
  308. {
  309. ThrowUserError('flag_not_multiplicable', { type => $flag_type });
  310. }
  311. # Make sure the requestees are authorized to access the bug
  312. # (and attachment, if this installation is using the "insider group"
  313. # feature and the attachment is marked private).
  314. if ($status eq '?' && $flag_type->is_requesteeble) {
  315. my $old_requestee = ($flag && $flag->requestee) ?
  316. $flag->requestee->login : '';
  317. my @legal_requestees;
  318. foreach my $login (@$requestees) {
  319. if ($login eq $old_requestee) {
  320. # This requestee was already set. Leave him alone.
  321. push(@legal_requestees, $login);
  322. next;
  323. }
  324. # We know the requestee exists because we ran
  325. # Bugzilla::User::match_field before getting here.
  326. my $requestee = new Bugzilla::User({ name => $login });
  327. # Throw an error if the user can't see the bug.
  328. # Note that if permissions on this bug are changed,
  329. # can_see_bug() will refer to old settings.
  330. if (!$requestee->can_see_bug($bug_id)) {
  331. next if $skip_requestee_on_error;
  332. ThrowUserError('flag_requestee_unauthorized',
  333. { flag_type => $flag_type,
  334. requestee => $requestee,
  335. bug_id => $bug_id,
  336. attach_id => $attach_id });
  337. }
  338. # Throw an error if the target is a private attachment and
  339. # the requestee isn't in the group of insiders who can see it.
  340. if ($attach_id
  341. && $private_attachment
  342. && Bugzilla->params->{'insidergroup'}
  343. && !$requestee->in_group(Bugzilla->params->{'insidergroup'}))
  344. {
  345. next if $skip_requestee_on_error;
  346. ThrowUserError('flag_requestee_unauthorized_attachment',
  347. { flag_type => $flag_type,
  348. requestee => $requestee,
  349. bug_id => $bug_id,
  350. attach_id => $attach_id });
  351. }
  352. # Throw an error if the user won't be allowed to set the flag.
  353. if (!$requestee->can_set_flag($flag_type)) {
  354. next if $skip_requestee_on_error;
  355. ThrowUserError('flag_requestee_needs_privs',
  356. {'requestee' => $requestee,
  357. 'flagtype' => $flag_type});
  358. }
  359. # This requestee can be set.
  360. push(@legal_requestees, $login);
  361. }
  362. # Update the requestee list for this flag.
  363. if (scalar(@legal_requestees) < scalar(@$requestees)) {
  364. my $field_name = 'requestee_type-' . $flag_type->id;
  365. Bugzilla->cgi->delete($field_name);
  366. Bugzilla->cgi->param(-name => $field_name, -value => \@legal_requestees);
  367. }
  368. }
  369. # Make sure the user is authorized to modify flags, see bug 180879
  370. # - The flag exists and is unchanged.
  371. return if ($flag && ($status eq $flag->status));
  372. # - User in the request_group can clear pending requests and set flags
  373. # and can rerequest set flags.
  374. return if (($status eq 'X' || $status eq '?')
  375. && $setter->can_request_flag($flag_type));
  376. # - User in the grant_group can set/clear flags, including "+" and "-".
  377. return if $setter->can_set_flag($flag_type);
  378. # - Any other flag modification is denied
  379. ThrowUserError('flag_update_denied',
  380. { name => $flag_type->name,
  381. status => $status,
  382. old_status => $flag ? $flag->status : 'X' });
  383. }
  384. sub snapshot {
  385. my ($class, $bug_id, $attach_id) = @_;
  386. my $flags = $class->match({ 'bug_id' => $bug_id,
  387. 'attach_id' => $attach_id });
  388. my @summaries;
  389. foreach my $flag (@$flags) {
  390. my $summary = $flag->type->name . $flag->status;
  391. $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee;
  392. push(@summaries, $summary);
  393. }
  394. return @summaries;
  395. }
  396. =pod
  397. =over
  398. =item C<process($bug, $attachment, $timestamp, $hr_vars)>
  399. Processes changes to flags.
  400. The bug and/or the attachment objects are the ones this flag is about,
  401. the timestamp is the date/time the bug was last touched (so that changes
  402. to the flag can be stamped with the same date/time).
  403. =back
  404. =cut
  405. sub process {
  406. my ($class, $bug, $attachment, $timestamp, $hr_vars) = @_;
  407. my $dbh = Bugzilla->dbh;
  408. my $cgi = Bugzilla->cgi;
  409. # Make sure the bug (and attachment, if given) exists and is accessible
  410. # to the current user. Moreover, if an attachment object is passed,
  411. # make sure it belongs to the given bug.
  412. return if ($bug->error || ($attachment && $bug->bug_id != $attachment->bug_id));
  413. my $bug_id = $bug->bug_id;
  414. my $attach_id = $attachment ? $attachment->id : undef;
  415. # Use the date/time we were given if possible (allowing calling code
  416. # to synchronize the comment's timestamp with those of other records).
  417. $timestamp ||= $dbh->selectrow_array('SELECT NOW()');
  418. # Take a snapshot of flags before any changes.
  419. my @old_summaries = $class->snapshot($bug_id, $attach_id);
  420. # Cancel pending requests if we are obsoleting an attachment.
  421. if ($attachment && $cgi->param('isobsolete')) {
  422. $class->CancelRequests($bug, $attachment);
  423. }
  424. # Create new flags and update existing flags.
  425. my $new_flags = FormToNewFlags($bug, $attachment, $cgi, $hr_vars);
  426. foreach my $flag (@$new_flags) { create($flag, $bug, $attachment, $timestamp) }
  427. modify($bug, $attachment, $cgi, $timestamp);
  428. # In case the bug's product/component has changed, clear flags that are
  429. # no longer valid.
  430. my $flag_ids = $dbh->selectcol_arrayref(
  431. "SELECT DISTINCT flags.id
  432. FROM flags
  433. INNER JOIN bugs
  434. ON flags.bug_id = bugs.bug_id
  435. LEFT JOIN flaginclusions AS i
  436. ON flags.type_id = i.type_id
  437. AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
  438. AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
  439. WHERE bugs.bug_id = ?
  440. AND i.type_id IS NULL",
  441. undef, $bug_id);
  442. my $flags = Bugzilla::Flag->new_from_list($flag_ids);
  443. foreach my $flag (@$flags) {
  444. my $is_retargetted = retarget($flag, $bug);
  445. unless ($is_retargetted) {
  446. clear($flag, $bug, $flag->attachment);
  447. $hr_vars->{'message'} = 'flag_cleared';
  448. }
  449. }
  450. $flag_ids = $dbh->selectcol_arrayref(
  451. "SELECT DISTINCT flags.id
  452. FROM flags, bugs, flagexclusions e
  453. WHERE bugs.bug_id = ?
  454. AND flags.bug_id = bugs.bug_id
  455. AND flags.type_id = e.type_id
  456. AND (bugs.product_id = e.product_id OR e.product_id IS NULL)
  457. AND (bugs.component_id = e.component_id OR e.component_id IS NULL)",
  458. undef, $bug_id);
  459. $flags = Bugzilla::Flag->new_from_list($flag_ids);
  460. foreach my $flag (@$flags) {
  461. my $is_retargetted = retarget($flag, $bug);
  462. clear($flag, $bug, $flag->attachment) unless $is_retargetted;
  463. }
  464. # Take a snapshot of flags after changes.
  465. my @new_summaries = $class->snapshot($bug_id, $attach_id);
  466. update_activity($bug_id, $attach_id, $timestamp, \@old_summaries, \@new_summaries);
  467. Bugzilla::Hook::process('flag-end_of_update', { bug => $bug,
  468. timestamp => $timestamp,
  469. old_flags => \@old_summaries,
  470. new_flags => \@new_summaries,
  471. });
  472. }
  473. sub update_activity {
  474. my ($bug_id, $attach_id, $timestamp, $old_summaries, $new_summaries) = @_;
  475. my $dbh = Bugzilla->dbh;
  476. $old_summaries = join(", ", @$old_summaries);
  477. $new_summaries = join(", ", @$new_summaries);
  478. my ($removed, $added) = diff_strings($old_summaries, $new_summaries);
  479. if ($removed ne $added) {
  480. my $field_id = get_field_id('flagtypes.name');
  481. $dbh->do('INSERT INTO bugs_activity
  482. (bug_id, attach_id, who, bug_when, fieldid, removed, added)
  483. VALUES (?, ?, ?, ?, ?, ?, ?)',
  484. undef, ($bug_id, $attach_id, Bugzilla->user->id,
  485. $timestamp, $field_id, $removed, $added));
  486. $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
  487. undef, ($timestamp, $bug_id));
  488. }
  489. }
  490. =pod
  491. =over
  492. =item C<create($flag, $bug, $attachment, $timestamp)>
  493. Creates a flag record in the database.
  494. =back
  495. =cut
  496. sub create {
  497. my ($flag, $bug, $attachment, $timestamp) = @_;
  498. my $dbh = Bugzilla->dbh;
  499. my $attach_id = $attachment ? $attachment->id : undef;
  500. my $requestee_id;
  501. # Be careful! At this point, $flag is *NOT* yet an object!
  502. $requestee_id = $flag->{'requestee'}->id if $flag->{'requestee'};
  503. $dbh->do('INSERT INTO flags (type_id, bug_id, attach_id, requestee_id,
  504. setter_id, status, creation_date, modification_date)
  505. VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
  506. undef, ($flag->{'type'}->id, $bug->bug_id,
  507. $attach_id, $requestee_id, $flag->{'setter'}->id,
  508. $flag->{'status'}, $timestamp, $timestamp));
  509. # Now that the new flag has been added to the DB, create a real flag object.
  510. # This is required to call notify() correctly.
  511. my $flag_id = $dbh->bz_last_key('flags', 'id');
  512. $flag = new Bugzilla::Flag($flag_id);
  513. # Send an email notifying the relevant parties about the flag creation.
  514. if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
  515. $flag->{'addressee'} = $flag->requestee;
  516. }
  517. notify($flag, $bug, $attachment);
  518. # Return the new flag object.
  519. return $flag;
  520. }
  521. =pod
  522. =over
  523. =item C<modify($bug, $attachment, $cgi, $timestamp)>
  524. Modifies flags in the database when a user changes them.
  525. =back
  526. =cut
  527. sub modify {
  528. my ($bug, $attachment, $cgi, $timestamp) = @_;
  529. my $setter = Bugzilla->user;
  530. my $dbh = Bugzilla->dbh;
  531. # Extract a list of flags from the form data.
  532. my @ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
  533. # Loop over flags and update their record in the database if necessary.
  534. # Two kinds of changes can happen to a flag: it can be set to a different
  535. # state, and someone else can be asked to set it. We take care of both
  536. # those changes.
  537. my @flags;
  538. foreach my $id (@ids) {
  539. my $flag = new Bugzilla::Flag($id);
  540. # If the flag no longer exists, ignore it.
  541. next unless $flag;
  542. my $status = $cgi->param("flag-$id");
  543. # If the user entered more than one name into the requestee field
  544. # (i.e. they want more than one person to set the flag) we can reuse
  545. # the existing flag for the first person (who may well be the existing
  546. # requestee), but we have to create new flags for each additional.
  547. my @requestees = $cgi->param("requestee-$id");
  548. my $requestee_email;
  549. if ($status eq "?"
  550. && scalar(@requestees) > 1
  551. && $flag->type->is_multiplicable)
  552. {
  553. # The first person, for which we'll reuse the existing flag.
  554. $requestee_email = shift(@requestees);
  555. # Create new flags like the existing one for each additional person.
  556. foreach my $login (@requestees) {
  557. create({ type => $flag->type,
  558. setter => $setter,
  559. status => "?",
  560. requestee => new Bugzilla::User({ name => $login }) },
  561. $bug, $attachment, $timestamp);
  562. }
  563. }
  564. else {
  565. $requestee_email = trim($cgi->param("requestee-$id") || '');
  566. }
  567. # Ignore flags the user didn't change. There are two components here:
  568. # either the status changes (trivial) or the requestee changes.
  569. # Change of either field will cause full update of the flag.
  570. my $status_changed = ($status ne $flag->status);
  571. # Requestee is considered changed, if all of the following apply:
  572. # 1. Flag status is '?' (requested)
  573. # 2. Flag can have a requestee
  574. # 3. The requestee specified on the form is different from the
  575. # requestee specified in the db.
  576. my $old_requestee = $flag->requestee ? $flag->requestee->login : '';
  577. my $requestee_changed =
  578. ($status eq "?" &&
  579. $flag->type->is_requesteeble &&
  580. $old_requestee ne $requestee_email);
  581. next unless ($status_changed || $requestee_changed);
  582. # Since the status is validated, we know it's safe, but it's still
  583. # tainted, so we have to detaint it before using it in a query.
  584. trick_taint($status);
  585. if ($status eq '+' || $status eq '-') {
  586. $dbh->do('UPDATE flags
  587. SET setter_id = ?, requestee_id = NULL,
  588. status = ?, modification_date = ?
  589. WHERE id = ?',
  590. undef, ($setter->id, $status, $timestamp, $flag->id));
  591. # If the status of the flag was "?", we have to notify
  592. # the requester (if he wants to).
  593. my $requester;
  594. if ($flag->status eq '?') {
  595. $requester = $flag->setter;
  596. $flag->{'requester'} = $requester;
  597. }
  598. # Now update the flag object with its new values.
  599. $flag->{'setter'} = $setter;
  600. $flag->{'requestee'} = undef;
  601. $flag->{'requestee_id'} = undef;
  602. $flag->{'status'} = $status;
  603. # Send an email notifying the relevant parties about the fulfillment,
  604. # including the requester.
  605. if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
  606. $flag->{'addressee'} = $requester;
  607. }
  608. notify($flag, $bug, $attachment);
  609. }
  610. elsif ($status eq '?') {
  611. # If the one doing the change is the requestee, then this means he doesn't
  612. # want to reply to the request and he simply reassigns the request to
  613. # someone else. In this case, we keep the requester unaltered.
  614. my $new_setter = $setter;
  615. if ($flag->requestee && $flag->requestee->id == $setter->id) {
  616. $new_setter = $flag->setter;
  617. }
  618. # Get the requestee, if any.
  619. my $requestee_id;
  620. if ($requestee_email) {
  621. $requestee_id = login_to_id($requestee_email);
  622. $flag->{'requestee'} = new Bugzilla::User($requestee_id);
  623. $flag->{'requestee_id'} = $requestee_id;
  624. }
  625. else {
  626. # If the status didn't change but we only removed the
  627. # requestee, we have to clear the requestee field.
  628. $flag->{'requestee'} = undef;
  629. $flag->{'requestee_id'} = undef;
  630. }
  631. # Update the database with the changes.
  632. $dbh->do('UPDATE flags
  633. SET setter_id = ?, requestee_id = ?,
  634. status = ?, modification_date = ?
  635. WHERE id = ?',
  636. undef, ($new_setter->id, $requestee_id, $status,
  637. $timestamp, $flag->id));
  638. # Now update the flag object with its new values.
  639. $flag->{'setter'} = $new_setter;
  640. $flag->{'status'} = $status;
  641. # Send an email notifying the relevant parties about the request.
  642. if ($flag->requestee && $flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) {
  643. $flag->{'addressee'} = $flag->requestee;
  644. }
  645. notify($flag, $bug, $attachment);
  646. }
  647. elsif ($status eq 'X') {
  648. clear($flag, $bug, $attachment);
  649. }
  650. push(@flags, $flag);
  651. }
  652. return \@flags;
  653. }
  654. =pod
  655. =over
  656. =item C<retarget($flag, $bug)>
  657. Change the type of the flag, if possible. The new flag type must have
  658. the same name as the current flag type, must exist in the product and
  659. component the bug is in, and the current settings of the flag must pass
  660. validation. If no such flag type can be found, the type remains unchanged.
  661. Retargetting flags is a good way to keep flags when moving bugs from one
  662. product where a flag type is available to another product where the flag
  663. type is unavailable, but another flag type having the same name exists.
  664. Most of the time, if they have the same name, this means that they have
  665. the same meaning, but with different settings.
  666. =back
  667. =cut
  668. sub retarget {
  669. my ($flag, $bug) = @_;
  670. my $dbh = Bugzilla->dbh;
  671. # We are looking for flagtypes having the same name as the flagtype
  672. # to which the current flag belongs, and being in the new product and
  673. # component of the bug.
  674. my $flagtypes = Bugzilla::FlagType::match(
  675. {'name' => $flag->name,
  676. 'target_type' => $flag->type->target_type,
  677. 'is_active' => 1,
  678. 'product_id' => $bug->product_id,
  679. 'component_id' => $bug->component_id});
  680. # If we found no flagtype, the flag will be deleted.
  681. return 0 unless scalar(@$flagtypes);
  682. # If we found at least one, change the type of the flag,
  683. # assuming the setter/requester is allowed to set/request flags
  684. # belonging to this flagtype.
  685. my $requestee = $flag->requestee ? [$flag->requestee->login] : [];
  686. my $is_private = ($flag->attachment) ? $flag->attachment->isprivate : 0;
  687. my $is_retargetted = 0;
  688. foreach my $flagtype (@$flagtypes) {
  689. # Get the number of flags of this type already set for this target.
  690. my $has_flags = __PACKAGE__->count(
  691. { 'type_id' => $flagtype->id,
  692. 'bug_id' => $bug->bug_id,
  693. 'attach_id' => $flag->attach_id });
  694. # Do not create a new flag of this type if this flag type is
  695. # not multiplicable and already has a flag set.
  696. next if (!$flagtype->is_multiplicable && $has_flags);
  697. # Check user privileges.
  698. my $error_mode_cache = Bugzilla->error_mode;
  699. Bugzilla->error_mode(ERROR_MODE_DIE);
  700. eval {
  701. _validate(undef, $flagtype, $flag->status, $flag->setter,
  702. $requestee, $is_private, $bug->bug_id, $flag->attach_id);
  703. };
  704. Bugzilla->error_mode($error_mode_cache);
  705. # If the validation failed, then we cannot use this flagtype.
  706. next if ($@);
  707. # Checks are successful, we can retarget the flag to this flagtype.
  708. $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?',
  709. undef, ($flagtype->id, $flag->id));
  710. $is_retargetted = 1;
  711. last;
  712. }
  713. return $is_retargetted;
  714. }
  715. =pod
  716. =over
  717. =item C<clear($flag, $bug, $attachment)>
  718. Remove a flag from the DB.
  719. =back
  720. =cut
  721. sub clear {
  722. my ($flag, $bug, $attachment) = @_;
  723. my $dbh = Bugzilla->dbh;
  724. $dbh->do('DELETE FROM flags WHERE id = ?', undef, $flag->id);
  725. # If we cancel a pending request, we have to notify the requester
  726. # (if he wants to).
  727. my $requester;
  728. if ($flag->status eq '?') {
  729. $requester = $flag->setter;
  730. $flag->{'requester'} = $requester;
  731. }
  732. # Now update the flag object to its new values. The last
  733. # requester/setter and requestee are kept untouched (for the
  734. # record). Else we could as well delete the flag completely.
  735. $flag->{'exists'} = 0;
  736. $flag->{'status'} = "X";
  737. if ($requester && $requester->wants_mail([EVT_REQUESTED_FLAG])) {
  738. $flag->{'addressee'} = $requester;
  739. }
  740. notify($flag, $bug, $attachment);
  741. }
  742. ######################################################################
  743. # Utility Functions
  744. ######################################################################
  745. =pod
  746. =over
  747. =item C<FormToNewFlags($bug, $attachment, $cgi, $hr_vars)>
  748. Checks whether or not there are new flags to create and returns an
  749. array of flag objects. This array is then passed to Flag::create().
  750. =back
  751. =cut
  752. sub FormToNewFlags {
  753. my ($bug, $attachment, $cgi, $hr_vars) = @_;
  754. my $dbh = Bugzilla->dbh;
  755. my $setter = Bugzilla->user;
  756. # Extract a list of flag type IDs from field names.
  757. my @type_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
  758. @type_ids = grep($cgi->param("flag_type-$_") ne 'X', @type_ids);
  759. return () unless scalar(@type_ids);
  760. # Get a list of active flag types available for this product/component.
  761. my $flag_types = Bugzilla::FlagType::match(
  762. { 'product_id' => $bug->{'product_id'},
  763. 'component_id' => $bug->{'component_id'},
  764. 'is_active' => 1 });
  765. foreach my $type_id (@type_ids) {
  766. # Checks if there are unexpected flags for the product/component.
  767. if (!scalar(grep { $_->id == $type_id } @$flag_types)) {
  768. $hr_vars->{'message'} = 'unexpected_flag_types';
  769. last;
  770. }
  771. }
  772. my @flags;
  773. foreach my $flag_type (@$flag_types) {
  774. my $type_id = $flag_type->id;
  775. # Bug flags are only valid for bugs, and attachment flags are
  776. # only valid for attachments. So don't mix both.
  777. next unless ($flag_type->target_type eq 'bug' xor $attachment);
  778. # We are only interested in flags the user tries to create.
  779. next unless scalar(grep { $_ == $type_id } @type_ids);
  780. # Get the number of flags of this type already set for this target.
  781. my $has_flags = __PACKAGE__->count(
  782. { 'type_id' => $type_id,
  783. 'target_type' => $attachment ? 'attachment' : 'bug',
  784. 'bug_id' => $bug->bug_id,
  785. 'attach_id' => $attachment ? $attachment->id : undef });
  786. # Do not create a new flag of this type if this flag type is
  787. # not multiplicable and already has a flag set.
  788. next if (!$flag_type->is_multiplicable && $has_flags);
  789. my $status = $cgi->param("flag_type-$type_id");
  790. trick_taint($status);
  791. my @logins = $cgi->param("requestee_type-$type_id");
  792. if ($status eq "?" && scalar(@logins) > 0) {
  793. foreach my $login (@logins) {
  794. push (@flags, { type => $flag_type ,
  795. setter => $setter ,
  796. status => $status ,
  797. requestee =>
  798. new Bugzilla::User({ name => $login }) });
  799. last unless $flag_type->is_multiplicable;
  800. }
  801. }
  802. else {
  803. push (@flags, { type => $flag_type ,
  804. setter => $setter ,
  805. status => $status });
  806. }
  807. }
  808. # Return the list of flags.
  809. return \@flags;
  810. }
  811. =pod
  812. =over
  813. =item C<notify($flag, $bug, $attachment)>
  814. Sends an email notification about a flag being created, fulfilled
  815. or deleted.
  816. =back
  817. =cut
  818. sub notify {
  819. my ($flag, $bug, $attachment) = @_;
  820. #if WEBKIT_CHANGES
  821. # Don't send a notification when the flag is in-rietveld,
  822. # since it isn't a user visible flag, and that mail is spammy.
  823. return if ($flag->type->name eq 'in-rietveld');
  824. #endif // WEBKIT_CHANGES
  825. # There is nobody to notify.
  826. return unless ($flag->{'addressee'} || $flag->type->cc_list);
  827. # If the target bug is restricted to one or more groups, then we need
  828. # to make sure we don't send email about it to unauthorized users
  829. # on the request type's CC: list, so we have to trawl the list for users
  830. # not in those groups or email addresses that don't have an account.
  831. my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups};
  832. my $attachment_is_private = $attachment ? $attachment->isprivate : undef;
  833. my %recipients;
  834. foreach my $cc (split(/[, ]+/, $flag->type->cc_list)) {
  835. my $ccuser = new Bugzilla::User({ name => $cc });
  836. next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id)));
  837. next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider);
  838. # Prevent duplicated entries due to case sensitivity.
  839. $cc = $ccuser ? $ccuser->email : $cc;
  840. $recipients{$cc} = $ccuser;
  841. }
  842. # Only notify if the addressee is allowed to receive the email.
  843. if ($flag->{'addressee'} && $flag->{'addressee'}->email_enabled) {
  844. $recipients{$flag->{'addressee'}->email} = $flag->{'addressee'};
  845. }
  846. # Process and send notification for each recipient.
  847. # If there are users in the CC list who don't have an account,
  848. # use the default language for email notifications.
  849. my $default_lang;
  850. if (grep { !$_ } values %recipients) {
  851. my $default_user = new Bugzilla::User();
  852. $default_lang = $default_user->settings->{'lang'}->{'value'};
  853. }
  854. foreach my $to (keys %recipients) {
  855. # Add threadingmarker to allow flag notification emails to be the
  856. # threaded similar to normal bug change emails.
  857. my $user_id = $recipients{$to} ? $recipients{$to}->id : 0;
  858. my $threadingmarker = build_thread_marker($bug->id, $user_id);
  859. my $vars = { 'flag' => $flag,
  860. 'to' => $to,
  861. 'bug' => $bug,
  862. 'attachment' => $attachment,
  863. 'threadingmarker' => $threadingmarker };
  864. my $lang = $recipients{$to} ?
  865. $recipients{$to}->settings->{'lang'}->{'value'} : $default_lang;
  866. my $template = Bugzilla->template_inner($lang);
  867. my $message;
  868. $template->process("request/email.txt.tmpl", $vars, \$message)
  869. || ThrowTemplateError($template->error());
  870. Bugzilla->template_inner("");
  871. MessageToMTA($message);
  872. }
  873. }
  874. # Cancel all request flags from the attachment being obsoleted.
  875. sub CancelRequests {
  876. my ($class, $bug, $attachment, $timestamp) = @_;
  877. my $dbh = Bugzilla->dbh;
  878. my $request_ids =
  879. $dbh->selectcol_arrayref("SELECT flags.id
  880. FROM flags
  881. LEFT JOIN attachments ON flags.attach_id = attachments.attach_id
  882. WHERE flags.attach_id = ?
  883. AND flags.status = '?'
  884. AND attachments.isobsolete = 0",
  885. undef, $attachment->id);
  886. return if (!scalar(@$request_ids));
  887. # Take a snapshot of flags before any changes.
  888. my @old_summaries = $class->snapshot($bug->bug_id, $attachment->id)
  889. if ($timestamp);
  890. my $flags = Bugzilla::Flag->new_from_list($request_ids);
  891. foreach my $flag (@$flags) { clear($flag, $bug, $attachment) }
  892. # If $timestamp is undefined, do not update the activity table
  893. return unless ($timestamp);
  894. # Take a snapshot of flags after any changes.
  895. my @new_summaries = $class->snapshot($bug->bug_id, $attachment->id);
  896. update_activity($bug->bug_id, $attachment->id, $timestamp,
  897. \@old_summaries, \@new_summaries);
  898. }
  899. =head1 SEE ALSO
  900. =over
  901. =item B<Bugzilla::FlagType>
  902. =back
  903. =head1 CONTRIBUTORS
  904. =over
  905. =item Myk Melez <myk@mozilla.org>
  906. =item Jouni Heikniemi <jouni@heikniemi.net>
  907. =item Kevin Benton <kevin.benton@amd.com>
  908. =item Frédéric Buclin <LpSolit@gmail.com>
  909. =back
  910. =cut
  911. 1;