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