r1389: Forgot to add the template diff
[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 // reset all the data each time we do this, just in case it changes within the lifespan of the object
157 $this->users = array();
158 foreach ($this->roles AS $role => $users)
159 {
160 $this->roles["$role"] = array();
161 }
162
163 $newbuggers = $this->registry->db->query("SELECT userid FROM " . TABLE_PREFIX . "useremail WHERE relation = " . $this->registry->emailoptions['relations']['-notapplicable-'] . " AND mask & " . $this->registry->emailoptions['notifications']['newbug']);
164 while ($newbug = $this->registry->db->fetch_array($newbuggers))
165 {
166 $this->roles['-notapplicable-']["$newbug[userid]"] = $newbug['userid'];
167 }
168
169 $favorites = $this->registry->db->query("SELECT userid FROM " . TABLE_PREFIX . "favorite WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
170 while ($fav = $this->registry->db->fetch_array($favorites))
171 {
172 $this->roles['favorite']["$fav[userid]"] = $fav['userid'];
173 }
174
175 $voters = $this->registry->db->query_first("SELECT userids FROM " . TABLE_PREFIX . "vote WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
176 $this->roles['voter'] = preg_split('#,#', $voters['userids'], 0, PREG_SPLIT_NO_EMPTY);
177
178 $commenters = $this->registry->db->query("SELECT userid FROM " . TABLE_PREFIX . "comment WHERE bugid = " . $this->registry->clean($this->bug['bugid'], TYPE_UINT));
179 while ($comment = $this->registry->db->fetch_array($commenters))
180 {
181 $this->roles['commenter']["$comment[userid]"] = $comment['userid'];
182 }
183
184 $masterids = array_merge($this->roles['-notapplicable-'], $this->roles['reporter'], $this->roles['assignee'], $this->roles['favorite'], $this->roles['voter'], $this->roles['commenter']);
185 $masterids = $this->registry->funct->array_strip_empty(array_unique($masterids));
186
187 if (is_array($masterids) AND sizeof($masterids) > 0)
188 {
189 $userinfo = $this->registry->db->query("
190 SELECT user.*, useremail.*
191 FROM " . TABLE_PREFIX . "useremail AS useremail
192 LEFT JOIN " . TABLE_PREFIX . "user AS user
193 ON (user.userid = useremail.userid)
194 WHERE useremail.userid IN (" . implode(',', $masterids) . ")
195 ");
196 while ($user = $this->registry->db->fetch_array($userinfo))
197 {
198 if (!is_array($this->users["$user[userid]"]))
199 {
200 $this->users["$user[userid]"] = $user;
201 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
202 }
203 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
204 }
205 }
206 }
207
208 // ###################################################################
209 /**
210 * Sends the appropriate emails for changes to bugs. This function
211 * works a lot like the Logging class by taking BugAPI->objdata and
212 * BugAPI->values and then comparing the two arries and sending emails
213 * with the differences.
214 *
215 * @access public
216 *
217 * @param array Original custom fields data
218 * @param array Modified custom fields data
219 */
220 function send_bug_changes_notice($original, $modified)
221 {
222 if (!isset($this->modified['bugid']))
223 {
224 return;
225 }
226
227 // fields with custom mask information
228 if ($this->original['assignedto'] != $this->modified['assignedto'])
229 {
230 if ($this->original['assignedto'] != '')
231 {
232 $this->notice_no_longer_assigned($this->original['assignedto']);
233 }
234 if ($this->modified['assignedto'] != '')
235 {
236 $this->notice_now_assigned($this->modified['assignedto']);
237 }
238 }
239 if ($this->original['status'] != $this->modified['status'])
240 {
241 $this->notice_status_change($this->original['status'], $this->modified['status']);
242 }
243 if ($this->original['resolution'] != $this->modified['resolution'])
244 {
245 $this->notice_resolution_change($this->original['resolution'], $this->modified['resolution']);
246 }
247 if ($this->original['duplicates'] != $this->modified['duplicates'])
248 {
249 $this->notice_duplicates_change($this->original['duplicates'], $this->modified['duplicates']);
250 }
251
252 // other standard fields that don't have custom masks
253 if ($this->original['severity'] != $this->modified['severity'])
254 {
255 $this->notice_severity_change($this->original['severity'], $this->modified['severity']);
256 }
257 if ($this->original['priority'] != $this->modified['priority'])
258 {
259 $this->notice_priority_change($this->original['priority'], $this->modified['priority']);
260 }
261 if (($this->original['product'] != $this->modified['product']) OR ($this->original['component'] != $this->modified['component']) OR ($this->original['version'] != $this->modified['version']))
262 {
263 $this->notice_pcv_change(array($this->original['product'], $this->original['component'], $this->original['version']), array($this->modified['product'], $this->modified['component'], $this->modified['version']));
264 }
265
266 $dofields = array(
267 'summary' => -1,
268 'dependency' => -1,
269 'hidden' => -1
270 );
271 foreach ($dofields AS $field => $lookup)
272 {
273 if ($this->original["$field"] != $this->modified["$field"])
274 {
275 $this->notice_other_change($field, $this->original["$field"], $this->modified["$field"]);
276 }
277 }
278
279 // custom field data
280 foreach ($modified AS $field => $value)
281 {
282 if ($field == 'bugid')
283 {
284 continue;
285 }
286 if ($original["$field"] != $modified["$field"])
287 {
288 $this->notice_other_change($field, $original["$field"], $modified["$field"]);
289 }
290 }
291 }
292
293 // ###################################################################
294 /**
295 * Sends an email to the specified user ID that they are no longer the
296 * person assigned to the bug.
297 *
298 * @access private
299 *
300 * @param integer User ID to send to
301 */
302 function notice_no_longer_assigned($userid)
303 {
304 if ($this->users["$userid"]['options'][0] & $this->registry->emailoptions['notifications']['assignedto'] AND in_array($userid, $this->roles['-notapplicable-']))
305 {
306 $this->notices["$userid"][] = sprintf(
307 _('You are no longer assigned to this bug, per %1$s\'s changes.'),
308
309 construct_user_display($this->registry->userinfo, false)
310 );
311 }
312 }
313
314 // ###################################################################
315 /**
316 * Informs the user that they have been made the assignee of the bug.
317 *
318 * @access private
319 *
320 * @param integer User ID
321 */
322 function notice_now_assigned($userid)
323 {
324 if ($this->users["$userid"]['options'][0] & $this->registry->emailoptions['notifications']['assignedto'] AND in_array($userid, $this->roles['-notapplicable-']))
325 {
326 $this->notices["$userid"][] = sprintf(
327 _('You have been assigned to this bug by %1$s.'),
328
329 construct_user_display($this->registry->userinfo, false)
330 );
331 }
332 }
333
334 // ###################################################################
335 /**
336 * Sends a message to inform users that the status has changed.
337 *
338 * @access private
339 *
340 * @param integer Old status
341 * @param integer New status
342 */
343 function notice_status_change($old, $new)
344 {
345 $userlist = $this->fetch_users_with_on_bit('statusresolve');
346 foreach ($userlist AS $userid => $user)
347 {
348 $this->notices["$user[userid]"][] = sprintf(
349 _('The status of the bug is now "%2$s", from "%1$s".'),
350
351 $this->registry->datastore['status']["$old"]['status'],
352 $this->registry->datastore['status']["$new"]['status']
353 );
354 }
355 }
356
357 // ###################################################################
358 /**
359 * Sends an email to inform users that the resolution has changed.
360 *
361 * @access private
362 *
363 * @param integer Old resolution
364 * @param integer New resolution
365 */
366 function notice_resolution_change($old, $new)
367 {
368 $userlist = $this->fetch_users_with_on_bit('statusresolve');
369 foreach ($userlist AS $userid => $user)
370 {
371 $this->notices["$user[userid]"][] = sprintf(
372 _('This bug has been resolved with resolution "%2$s", from "%1$s".'),
373
374 $this->registry->datastore['resolution']["$old"]['resolution'],
375 $this->registry->datastore['resolution']["$new"]['resolution']
376 );
377 }
378 }
379
380 // ###################################################################
381 /**
382 * Informs users that the duplicates list has changed.
383 *
384 * @access private
385 *
386 * @param string Old duplicates list
387 * @param string New duplicates list
388 */
389 function notice_duplicates_change($old, $new)
390 {
391 $userlist = $this->fetch_useres_with_on_bit('duplicates');
392 foreach ($userlist AS $userid => $user)
393 {
394 $this->notices["$user[userid]"][] = sprintf(
395 _('The duplicates list has changed from "%1$s" to %2$s".'),
396
397 $old,
398 $new
399 );
400 }
401 }
402
403 // ###################################################################
404 /**
405 * Sends an email to inform users that the severity has changed.
406 *
407 * @access private
408 *
409 * @param integer Old severity
410 * @param integer New severity
411 */
412 function notice_severity_change($old, $new)
413 {
414 $userlist = $this->fetch_users_with_on_bit('otherfield');
415 foreach ($userlist AS $userid => $user)
416 {
417 $this->notices["$user[userid]"][] = sprintf(
418 _('The severity has been elevated from "%1$s" to "%2$s".'),
419
420 $this->registry->datastore['severity']["$old"]['severity'],
421 $this->registry->datastore['severity']["$new"]['severity']
422 );
423 }
424 }
425
426 // ###################################################################
427 /**
428 * Informs users that the priority changed.
429 *
430 * @access private
431 *
432 * @param integer Old priority
433 * @param integer New priority
434 */
435 function notice_priority_change($old, $new)
436 {
437 $userlist = $this->fetch_users_with_on_bit('otherfield');
438 foreach ($userlist AS $userid => $user)
439 {
440 $this->notices["$user[userid]"][] = sprintf(
441 _('The priority has been elevatd from "%1$s" to "%2$s".'),
442
443 $this->registry->datastore['priority']["$old"]['priority'],
444 $this->registry->datastore['priority']["$new"]['priority']
445 );
446 }
447 }
448
449 // ###################################################################
450 /**
451 * Sends an email telling users that the product, component, or version
452 * has changed. This is done all at once because you really need to see
453 * the whole thing in the notice.
454 *
455 * @access private
456 *
457 * @param array Original PCV
458 * @param array Modified PCV
459 */
460 function notice_pcv_change($old, $new)
461 {
462 $userlist = $this->fetch_users_with_on_bit('otherfield');
463 foreach ($userlist AS $userid => $user)
464 {
465 $this->notices["$user[userid]"][] = sprintf(
466 _('The product, component, and version combination has changed from "%1$s" to "%2$s".'),
467
468 $this->registry->datastore['product']["$old[0]"]['title'] . '/' . ($old[1] ? $this->registry->datastore['product']["$old[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$old[2]"]['version'],
469 $this->registry->datastore['product']["$new[0]"]['title'] . '/' . ($new[1] ? $this->registry->datastore['product']["$new[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$new[2]"]['version']
470 );
471 }
472 }
473
474 // ###################################################################
475 /**
476 * Sends the appropriate users information about a new comment being
477 * posted to the bug report.
478 *
479 * @access public
480 *
481 * @param array CommentAPI->values array
482 */
483 function send_new_comment_notice($comment)
484 {
485 $userlist = $this->fetch_users_with_on_bit('newcomment');
486 foreach ($userlist AS $userid => $user)
487 {
488 $this->notices["$user[userid]"][] = sprintf(
489 _('The following comment was added by %1$s on %2$s:
490 ============================================
491 %3$s
492 ============================================'),
493
494 construct_user_display($this->registry->userinfo, false),
495 $this->registry->modules['date']->format($this->registry->options['dateformat'], $comment['dateline']),
496 $comment['comment']
497 );
498 }
499 }
500
501 // ###################################################################
502 /**
503 * A notice for an individual field changing.
504 *
505 * @access private
506 *
507 * @param string Field name
508 * @param mixed Original value
509 * @param mixed Modified value
510 */
511 function notice_other_change($name, $old, $new)
512 {
513 $userlist = $this->fetch_users_with_on_bit('otherfield');
514 foreach ($userlist AS $userid => $user)
515 {
516 $this->notices["$user[userid]"][] = sprintf(
517 _('The %1$s field changed from "%2$s" to "%3$s".'),
518
519 $name,
520 $old,
521 $new
522 );
523 }
524 }
525
526 // ###################################################################
527 /**
528 * Sends appropriate users a notice when a new attachment has been
529 * added.
530 *
531 * @access public
532 *
533 * @param array AttachmentAPI->values array
534 * @param array List of all attachments made obsolete
535 * @param array Newly-inserted attachment ID
536 */
537 function send_new_attachment_notice($attachment, $obsolete, $id)
538 {
539 $userlist = $this->fetch_users_with_on_bit('newattachment');
540 foreach ($userlist AS $userid => $user)
541 {
542 $this->notices["$userid"][] = sprintf(
543 _('%1$s has uploaded a new attachment:
544 ============================================
545 File name: %2$s
546 Description: %3$s
547 File size: %4$s Bytes
548 Makes obsolete: %5$s
549 View: %6$s
550 ============================================'),
551
552 construct_user_display($this->registry->userinfo, false),
553 $attachment['filename'],
554 $attachment['description'],
555 $attachment['filesize'],
556 implode(', ', (array)$obsolete),
557 $this->registry->options['trackerurl'] . '/viewattachment.php?attachmentid=' . $id
558 );
559 }
560 }
561
562 // ###################################################################
563 /**
564 * Sends a new bug notification notice to all those who have the option
565 * turned no. This does not use fetch_users_with_on_bit() because a
566 * query is more effective.
567 *
568 * @access public
569 *
570 * @param array Bug values array
571 * @param array Comment values array
572 */
573 function send_new_bug_notice($bug, $comment)
574 {
575 $userinfo = $this->registry->db->query("
576 SELECT user.*, useremail.*
577 FROM " . TABLE_PREFIX . "useremail AS useremail
578 LEFT JOIN " . TABLE_PREFIX . "user AS user
579 ON (user.userid = useremail.userid)
580 WHERE useremail.relation = 0
581 AND useremail.mask & " . $this->registry->emailoptions['notifications']['newbug'] . "
582 ");
583 while ($user = $this->registry->db->fetch_array($userinfo))
584 {
585 if (!is_array($this->users["$user[userid]"]))
586 {
587 $this->notices["$user[userid]"][] = sprintf(
588 _('
589 This bug has been added to the database:
590 ============================================
591 Bug ID: %1$s
592 Summary: %2$s
593 Reporter: %3$s
594 Product/Component/Version: %4$s
595 Initial report:
596 --------------------------------------------
597 %5$s
598 --------------------------------------------
599 ============================================'),
600 $bug['bugid'],
601 $bug['summary'],
602 construct_user_display($this->registry->userinfo, false),
603 $this->registry->datastore['product']["$bug[product]"]['title'] . '/' . ($bug['component'] ? $this->registry->datastore['product']["$bug[component]"]['title'] . '/' : '') . $this->registry->datastore['version']["$bug[version]"]['version'],
604 $comment['comment']
605 );
606 $this->users["$user[userid]"] = $user;
607 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
608 }
609 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
610 }
611 }
612
613 // ###################################################################
614 /**
615 * Generates an array of users who have a given email notification flag
616 * turned on in their bitfields.
617 *
618 * @access private
619 *
620 * @param string Notification bitfield name
621 *
622 * @return array Array of users and their data
623 */
624 function fetch_users_with_on_bit($bitname)
625 {
626 $idlist = array();
627
628 foreach ($this->users AS $user)
629 {
630 foreach ($this->registry->emailoptions['relations'] AS $name => $bit)
631 {
632 if (in_array($user['userid'], $this->roles["$name"]) AND $user['options']["$bit"] & $this->registry->emailoptions['notifications']["$bitname"])
633 {
634 $idlist[] = $user['userid'];
635 }
636 }
637 }
638
639 $masters = array_unique($idlist);
640
641 $return = array();
642 foreach ($masters AS $userid)
643 {
644 $return["$userid"] =& $this->users["$userid"];
645 }
646
647 return $return;
648 }
649
650 // ###################################################################
651 /**
652 * Compiles and sends the actual emails to users.
653 *
654 * @access public
655 */
656 function finalize()
657 {
658 // get the current bug for permissions checks
659 $bug = $this->registry->db->query_first("SELECT * FROM " . TABLE_PREFIX . "bug WHERE bugid = " . $this->bug['bugid']);
660 $this->registry->mail->setSubject(sprintf(_('%1$s Bug Notification - %2$s'), $this->registry->options['trackertitle'], $this->bug['summary']));
661 foreach ($this->notices AS $userid => $noticelist)
662 {
663 if ($userid == $this->registry->userinfo['userid'])
664 {
665 continue;
666 }
667
668 // we wouldn't want people who favorite bugs getting hidden notices
669 if (!check_bug_permissions($bug, $this->users["$userid"]))
670 {
671 $this->registry->debug("skipping user $userid ({$this->users[$userid]['email']}) because of permissions");
672 continue;
673 }
674
675 $this->registry->mail->setBodyText(sprintf(_('Hi %1$s,
676
677 You are receiving this email because you have opted to get notifications for the %2$s bug tracker.
678
679 The bug is "%5$s" (id: %6$s) located at %4$s/showreport.php?bugid=%6$s
680
681 Here are the notices:
682 ###################################################################
683
684 %3$s
685
686 ###################################################################
687 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.
688
689 %4$s'),
690 $this->users["$userid"]['displayname'],
691 $this->registry->options['trackertitle'],
692 implode("\n\n", $noticelist),
693 $this->registry->options['trackerurl'],
694 $this->bug['summary'],
695 $this->bug['bugid']
696 ));
697 if (!empty($this->users["$userid"]['email']))
698 {
699 $this->registry->mail->send($this->users["$userid"]['email'], $this->users["$userid"]['displayname']);
700 }
701 else
702 {
703 $this->registry->debug("not sending an email to " . $userid . " because they don't have one?");
704 }
705 }
706 }
707 }
708
709 /*=====================================================================*\
710 || ###################################################################
711 || # $HeadURL$
712 || # $Id$
713 || ###################################################################
714 \*=====================================================================*/
715 ?>