r1117: *FINALLY* fixing the NotificationCenter->finalize() permissions checking bug.
[bugdar.git] / includes / class_notification.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Bugdar [#]version[#]
5 || # Copyright ©2002-[#]year[#] Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version [#]gpl[#] of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Notification Center
24 *
25 * This class determines which emails need to be sent out based on user
26 * options and bug changes, and then it sends said emails.
27 *
28 * @author Blue Static
29 * @copyright Copyright ©2002 - [#]year[#], Blue Static
30 * @version $Revision$
31 * @package Bugdar
32 *
33 */
34 class NotificationCenter
35 {
36 /**
37 * Bug information
38 * @var array
39 * @access private
40 */
41 var $bug = array();
42
43 /**
44 * Original bug data
45 * @var array
46 * @access private
47 */
48 var $original = array();
49
50 /**
51 * Modified bug data
52 * @var array
53 * @access private
54 */
55 var $modified = array();
56
57 /**
58 * Global bugsys registry
59 * @var object
60 * @access private
61 */
62 var $registry = null;
63
64 /**
65 * Role list: a list of user IDs with their relations to the bug
66 * @var array
67 * @access private
68 */
69 var $roles = array(
70 '-notapplicable-' => array(),
71 'reporter' => array(),
72 'assignee' => array(),
73 'favorite' => array(),
74 'voter' => array(),
75 'commenter' => array()
76 );
77
78 /**
79 * User cache list
80 * @var array
81 * @access private
82 */
83 var $users = array();
84
85 /**
86 * A list of notices per-user that are combined together in NotificationCenter::finalize()
87 * @var array
88 * @access private
89 */
90 var $notices = array();
91
92 // ###################################################################
93 /**
94 * Constructor: set database objects
95 *
96 * @access public
97 */
98 function __construct()
99 {
100 global $bugsys;
101
102 $this->registry =& $bugsys;
103 }
104
105 // ###################################################################
106 /**
107 * (PHP 4) Constructor
108 *
109 * @access public
110 */
111 function NotificationCenter()
112 {
113 $this->__construct();
114 }
115
116 // ###################################################################
117 /**
118 * Sets the bug data so that all methods in this class have access to
119 * it when sending emails.
120 *
121 * @access public
122 *
123 * @param array Original bug data
124 * @param array Modified bug data
125 */
126 function set_bug_data($original, $modified = array())
127 {
128 if (sizeof($modified) > 0)
129 {
130 $this->bug = $modified;
131 }
132 else
133 {
134 $this->bug = $original;
135 }
136
137 $this->original = $original;
138 $this->modified = $modified;
139
140 $this->roles['-notapplicable-'] = (sizeof($modified) > 0 ? array($original['assignedto'], $modified['assignedto']) : array($original['assignedto']));
141 $this->roles['reporter'] = array($original['userid']);
142 $this->roles['assignee'] = (sizeof($modified) > 0 ? array($modified['assignedto']) : array($original['assignedto']));
143
144 $this->fetch_user_cache();
145 }
146
147 // ###################################################################
148 /**
149 * Fetches all the users who could be related to the bug and sticks
150 * their information into an array.
151 *
152 * @access private
153 */
154 function fetch_user_cache()
155 {
156 $favorites = $this->registry->db->query("SELECT userid FROM " . TABLE_PREFIX . "favorite WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
157 while ($fav = $this->registry->db->fetch_array($favorites))
158 {
159 $this->roles['favorite']["$fav[userid]"] = $fav['userid'];
160 }
161
162 $voters = $this->registry->db->query_first("SELECT userids FROM " . TABLE_PREFIX . "vote WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
163 $this->roles['voter'] = preg_split('#,#', $voters['userids'], 0, PREG_SPLIT_NO_EMPTY);
164
165 $commenters = $this->registry->db->query("SELECT userid FROM " . TABLE_PREFIX . "comment WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
166 while ($comment = $this->registry->db->fetch_array($commenters))
167 {
168 $this->roles['commenter']["$comment[userid]"] = $comment['userid'];
169 }
170
171 $masterids = array_merge($this->roles['-notapplicable-'], $this->roles['reporter'], $this->roles['assignee'], $this->roles['favorite'], $this->roles['voter'], $this->roles['commenter']);
172 $masterids = $this->registry->funct->array_strip_empty(array_unique($masterids));
173
174 $userinfo = $this->registry->db->query("
175 SELECT user.*, useremail.*
176 FROM " . TABLE_PREFIX . "useremail AS useremail
177 LEFT JOIN " . TABLE_PREFIX . "user AS user
178 ON (user.userid = useremail.userid)
179 WHERE useremail.userid IN (" . implode(',', $masterids) . ")
180 ");
181 while ($user = $this->registry->db->fetch_array($userinfo))
182 {
183 if (!is_array($this->users["$user[userid]"]))
184 {
185 $this->users["$user[userid]"] = $user;
186 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
187 }
188 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
189 }
190 }
191
192 // ###################################################################
193 /**
194 * Sends the appropriate emails for changes to bugs. This function
195 * works a lot like the Logging class by taking BugAPI->objdata and
196 * BugAPI->values and then comparing the two arries and sending emails
197 * with the differences.
198 *
199 * @access public
200 *
201 * @param array Original custom fields data
202 * @param array Modified custom fields data
203 */
204 function send_bug_changes_notice($original, $modified)
205 {
206 if (!isset($this->modified['bugid']))
207 {
208 return;
209 }
210
211 // fields with custom mask information
212 if ($this->original['assignedto'] != $this->modified['assignedto'])
213 {
214 if ($this->original['assignedto'] != '')
215 {
216 $this->notice_no_longer_assigned($this->original['assignedto']);
217 }
218 if ($this->modified['assignedto'] != '')
219 {
220 $this->notice_now_assigned($this->modified['assignedto']);
221 }
222 }
223 if ($this->original['status'] != $this->modified['status'])
224 {
225 $this->notice_status_change($this->original['status'], $this->modified['status']);
226 }
227 if ($this->original['resolution'] != $this->modified['resolution'])
228 {
229 $this->notice_resolution_change($this->original['resolution'], $this->modified['resolution']);
230 }
231 if ($this->original['duplicates'] != $this->modified['duplicates'])
232 {
233 $this->notice_duplicates_change($this->original['duplicates'], $this->modified['duplicates']);
234 }
235
236 // other standard fields that don't have custom masks
237 if ($this->original['severity'] != $this->modified['severity'])
238 {
239 $this->notice_severity_change($this->original['severity'], $this->modified['severity']);
240 }
241 if ($this->original['priority'] != $this->modified['priority'])
242 {
243 $this->notice_priority_change($this->original['priority'], $this->modified['priority']);
244 }
245 if (($this->original['product'] != $this->modified['product']) OR ($this->original['component'] != $this->modified['component']) OR ($this->original['version'] != $this->modified['version']))
246 {
247 $this->notice_pcv_change(array($this->original['product'], $this->original['component'], $this->original['version']), array($this->modified['product'], $this->modified['component'], $this->modified['version']));
248 }
249
250 $dofields = array(
251 'summary' => -1,
252 'dependency' => -1,
253 'hidden' => -1
254 );
255 foreach ($dofields AS $field => $lookup)
256 {
257 if ($this->original["$field"] != $this->modified["$field"])
258 {
259 $this->notice_other_change($field, $this->original["$field"], $this->modified["$field"]);
260 }
261 }
262
263 // custom field data
264 foreach ($modified AS $field => $value)
265 {
266 if ($field == 'bugid')
267 {
268 continue;
269 }
270 if ($original["$field"] != $modified["$field"])
271 {
272 $this->notice_other_change($field, $original["$field"], $modified["$field"]);
273 }
274 }
275 }
276
277 // ###################################################################
278 /**
279 * Sends an email to the specified user ID that they are no longer the
280 * person assigned to the bug.
281 *
282 * @access private
283 *
284 * @param integer User ID to send to
285 */
286 function notice_no_longer_assigned($userid)
287 {
288 if ($this->users["$userid"]['options'][0] & $this->registry->emailoptions['notifications']['assignedto'] AND in_array($userid, $this->roles['-notapplicable-']))
289 {
290 $this->notices["$userid"][] = sprintf(
291 _('You are no longer assigned to this bug, per %1$s\'s changes.'),
292
293 construct_user_display($this->registry->userinfo, false)
294 );
295 }
296 }
297
298 // ###################################################################
299 /**
300 * Informs the user that they have been made the assignee of the bug.
301 *
302 * @access private
303 *
304 * @param integer User ID
305 */
306 function notice_now_assigned($userid)
307 {
308 if ($this->users["$userid"]['options'][0] & $this->registry->emailoptions['notifications']['assignedto'] AND in_array($userid, $this->roles['-notapplicable-']))
309 {
310 $this->notices["$userid"][] = sprintf(
311 _('You have been assigned to this bug by %1$s.'),
312
313 construct_user_display($this->registry->userinfo, false)
314 );
315 }
316 }
317
318 // ###################################################################
319 /**
320 * Sends a message to inform users that the status has changed.
321 *
322 * @access private
323 *
324 * @param integer Old status
325 * @param integer New status
326 */
327 function notice_status_change($old, $new)
328 {
329 $userlist = $this->fetch_users_with_on_bit('statusresolve');
330 foreach ($userlist AS $userid => $user)
331 {
332 $this->notices["$user[userid]"][] = sprintf(
333 _('The status field has changed from "%1$s" to "%2$s".'),
334
335 $this->registry->datastore['status']["$old"]['status'],
336 $this->registry->datastore['status']["$new"]['status']
337 );
338 }
339 }
340
341 // ###################################################################
342 /**
343 * Sends an email to inform users that the resolution has changed.
344 *
345 * @access private
346 *
347 * @param integer Old resolution
348 * @param integer New resolution
349 */
350 function notice_resolution_change($old, $new)
351 {
352 $userlist = $this->fetch_users_with_on_bit('statusresolve');
353 foreach ($userlist AS $userid => $user)
354 {
355 $this->notices["$user[userid]"][] = sprintf(
356 _('The resolution field has changed from "%1$s" to "%2$s".'),
357
358 $this->registry->datastore['resolution']["$old"]['resolution'],
359 $this->registry->datastore['resolution']["$new"]['resolution']
360 );
361 }
362 }
363
364 // ###################################################################
365 /**
366 * Informs users that the duplicates list has changed.
367 *
368 * @access private
369 *
370 * @param string Old duplicates list
371 * @param string New duplicates list
372 */
373 function notice_duplicates_change($old, $new)
374 {
375 $userlist = $this->fetch_useres_with_on_bit('duplicates');
376 foreach ($userlist AS $userid => $user)
377 {
378 $this->notices["$user[userid]"][] = sprintf(
379 _('The duplicates list has changed from "%1$s" to %2$s".'),
380
381 $old,
382 $new
383 );
384 }
385 }
386
387 // ###################################################################
388 /**
389 * Sends an email to inform users that the severity has changed.
390 *
391 * @access private
392 *
393 * @param integer Old severity
394 * @param integer New severity
395 */
396 function notice_severity_change($old, $new)
397 {
398 $userlist = $this->fetch_users_with_on_bit('otherfield');
399 foreach ($userlist AS $userid => $user)
400 {
401 $this->notices["$user[userid]"][] = sprintf(
402 _('The severity field has changed from "%1$s" to "%2$s".'),
403
404 $this->registry->datastore['severity']["$old"]['severity'],
405 $this->registry->datastore['severity']["$new"]['severity']
406 );
407 }
408 }
409
410 // ###################################################################
411 /**
412 * Informs users that the priority changed.
413 *
414 * @access private
415 *
416 * @param integer Old priority
417 * @param integer New priority
418 */
419 function notice_priority_change($old, $new)
420 {
421 $userlist = $this->fetch_users_with_on_bit('otherfield');
422 foreach ($userlist AS $userid => $user)
423 {
424 $this->notices["$user[userid]"][] = sprintf(
425 _('The priority field has changed from "%1$s" to "%2$s".'),
426
427 $this->registry->datastore['priority']["$old"]['priority'],
428 $this->registry->datastore['priority']["$new"]['priority']
429 );
430 }
431 }
432
433 // ###################################################################
434 /**
435 * Sends an email telling users that the product, component, or version
436 * has changed. This is done all at once because you really need to see
437 * the whole thing in the notice.
438 *
439 * @access private
440 *
441 * @param array Original PCV
442 * @param array Modified PCV
443 */
444 function notice_pcv_change($old, $new)
445 {
446 $userlist = $this->fetch_users_with_on_bit('otherfield');
447 foreach ($userlist AS $userid => $user)
448 {
449 $this->notices["$user[userid]"][] = sprintf(
450 _('The product, component, and version combination has changed from "%1$s" to "%2$s".'),
451
452 $this->registry->datastore['product']["$old[0]"]['title'] . '/' . ($old[1] ? $this->registry->datastore['product']["$old[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$old[2]"]['version'],
453 $this->registry->datastore['product']["$new[0]"]['title'] . '/' . ($new[1] ? $this->registry->datastore['product']["$new[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$new[2]"]['version']
454 );
455 }
456 }
457
458 // ###################################################################
459 /**
460 * Sends the appropriate users information about a new comment being
461 * posted to the bug report.
462 *
463 * @access public
464 *
465 * @param array CommentAPI->values array
466 */
467 function send_new_comment_notice($comment)
468 {
469 $userlist = $this->fetch_users_with_on_bit('newcomment');
470 foreach ($userlist AS $userid => $user)
471 {
472 $this->notices["$user[userid]"][] = sprintf(
473 _('The following comment was added by %1$s on %2$s:
474 ============================================
475 %3$s
476 ============================================'),
477
478 construct_user_display($this->registry->userinfo, false),
479 $this->registry->modules['date']->format($this->registry->options['dateformat'], $comment['dateline']),
480 $comment['comment']
481 );
482 }
483 }
484
485 // ###################################################################
486 /**
487 * A notice for an individual field changing.
488 *
489 * @access private
490 *
491 * @param string Field name
492 * @param mixed Original value
493 * @param mixed Modified value
494 */
495 function notice_other_change($name, $old, $new)
496 {
497 $userlist = $this->fetch_users_with_on_bit('otherfield');
498 foreach ($userlist AS $userid => $user)
499 {
500 $this->notices["$user[userid]"][] = sprintf(
501 _('The %1$s field changed from "%2$s" to "%3$s".'),
502
503 $name,
504 $old,
505 $new
506 );
507 }
508 }
509
510 // ###################################################################
511 /**
512 * Sends appropriate users a notice when a new attachment has been
513 * added.
514 *
515 * @access public
516 *
517 * @param array AttachmentAPI->values array
518 * @param array List of all attachments made obsolete
519 * @param array Newly-inserted attachment ID
520 */
521 function send_new_attachment_notice($attachment, $obsolete, $id)
522 {
523 $userlist = $this->fetch_users_with_on_bit('newattachment');
524 foreach ($userlist AS $userid => $user)
525 {
526 $this->notices["$userid"][] = sprintf(
527 _('%1$s has uploaded a new attachment:
528 ============================================
529 File name: %2$s
530 Description: %3$s
531 File size: %4$s Bytes
532 Makes obsolete: %5$s
533 View: %6$s
534 ============================================'),
535
536 construct_user_display($this->registry->userinfo, false),
537 $attachment['filename'],
538 $attachment['description'],
539 $attachment['filesize'],
540 implode(', ', (array)$obsolete),
541 $this->registry->options['trackerurl'] . '/viewattachment.php?attachmentid=' . $id
542 );
543 }
544 }
545
546 // ###################################################################
547 /**
548 * Sends a new bug notification notice to all those who have the option
549 * turned no. This does not use fetch_users_with_on_bit() because a
550 * query is more effective.
551 *
552 * @access public
553 *
554 * @param array Bug values array
555 * @param array Comment values array
556 */
557 function send_new_bug_notice($bug, $comment)
558 {
559 $userinfo = $this->registry->db->query("
560 SELECT user.*, useremail.*
561 FROM " . TABLE_PREFIX . "useremail AS useremail
562 LEFT JOIN " . TABLE_PREFIX . "user AS user
563 ON (user.userid = useremail.userid)
564 WHERE useremail.relation = 0
565 AND useremail.mask & " . $this->registry->emailoptions['notifications']['newbug'] . "
566 ");
567 while ($user = $this->registry->db->fetch_array($userinfo))
568 {
569 if (!is_array($this->users["$user[userid]"]))
570 {
571 $this->notices["$user[userid]"][] = sprintf(
572 _('
573 This bug has been added to the database:
574 ============================================
575 Bug ID: %1$s
576 Summary: %2$s
577 Reporter: %3$s
578 Product/Component/Version: %4$s
579 Initial report:
580 --------------------------------------------
581 %5$s
582 --------------------------------------------
583 ============================================'),
584 $bug['bugid'],
585 $bug['summary'],
586 construct_user_display($this->registry->userinfo, false),
587 $this->registry->datastore['product']["$bug[product]"]['title'] . '/' . ($bug['component'] ? $this->registry->datastore['product']["$bug[component]"]['title'] . '/' : '') . $this->registry->datastore['version']["$bug[version]"]['version'],
588 $comment['comment']
589 );
590 $this->users["$user[userid]"] = $user;
591 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
592 }
593 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
594 }
595 }
596
597 // ###################################################################
598 /**
599 * Generates an array of users who have a given email notification flag
600 * turned on in their bitfields.
601 *
602 * @access private
603 *
604 * @param string Notification bitfield name
605 *
606 * @return array Array of users and their data
607 */
608 function fetch_users_with_on_bit($bitname)
609 {
610 $idlist = array();
611
612 foreach ($this->users AS $user)
613 {
614 foreach ($this->registry->emailoptions['relations'] AS $name => $bit)
615 {
616 if (in_array($user['userid'], $this->roles["$name"]) AND $user['options']["$bit"] & $this->registry->emailoptions['notifications']["$bitname"])
617 {
618 $idlist[] = $user['userid'];
619 }
620 }
621 }
622
623 $masters = array_unique($idlist);
624
625 $return = array();
626 foreach ($masters AS $userid)
627 {
628 $return["$userid"] =& $this->users["$userid"];
629 }
630
631 return $return;
632 }
633
634 // ###################################################################
635 /**
636 * Compiles and sends the actual emails to users.
637 *
638 * @access public
639 */
640 function finalize()
641 {
642 // get the current bug for permissions checks
643 $bug = $this->registry->db->query_first("SELECT * FROM " . TABLE_PREFIX . "bug WHERE bugid = " . $this->bug['bugid']);
644 $this->registry->mail->set('subject', sprintf(_('%1$s Bug Notification - %2$s'), $this->registry->options['trackertitle'], $this->bug['summary']));
645 foreach ($this->notices AS $userid => $noticelist)
646 {
647 if ($userid == $this->registry->userinfo['userid'])
648 {
649 continue;
650 }
651
652 // we wouldn't want people who favorite bugs getting hidden notices
653 if (!check_bug_permissions($bug, $this->users["$userid"]))
654 {
655 $this->registry->debug("skipping user $userid ({$this->users[$userid]['email']}) because of permissions");
656 continue;
657 }
658
659 $this->registry->mail->to_add($this->users["$userid"]['displayname'], $this->users["$userid"]['email']);
660 $this->registry->mail->set('bodytext', sprintf(_('Hi %1$s,
661
662 You are receiving this email because you have opted to get notifications for the %2$s bug tracker.
663
664 The bug is "%5$s" (id: %6$s) located at %4$s/showreport.php?bugid=%6$s
665
666 Here are the notices:
667 ###################################################################
668
669 %3$s
670
671 ###################################################################
672 If you no longer want to receive email from us, please log into your account and click the "My Controls" tab at the top of the screen to change email preferences.
673
674 %4$s'),
675 $this->users["$userid"]['displayname'],
676 $this->registry->options['trackertitle'],
677 implode("\n\n", $noticelist),
678 $this->registry->options['trackerurl'],
679 $this->bug['summary'],
680 $this->bug['bugid']
681 ));
682 $this->registry->mail->send(true);
683 }
684 }
685 }
686
687 /*=====================================================================*\
688 || ###################################################################
689 || # $HeadURL$
690 || # $Id$
691 || ###################################################################
692 \*=====================================================================*/
693 ?>