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