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