r1466: Fixing a bug in NotificationCenter::send_new_comment_notice() where we weren...
[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 function send_bug_changes_notice()
218 {
219 if (!isset($this->modified['bugid']))
220 {
221 return;
222 }
223
224 // fields with custom mask information
225 if ($this->original['assignedto'] != $this->modified['assignedto'])
226 {
227 if ($this->original['assignedto'] != '')
228 {
229 $this->notice_no_longer_assigned($this->original['assignedto']);
230 }
231 if ($this->modified['assignedto'] != '')
232 {
233 $this->notice_now_assigned($this->modified['assignedto']);
234 }
235 }
236 if ($this->original['status'] != $this->modified['status'])
237 {
238 $this->notice_status_change($this->original['status'], $this->modified['status']);
239 }
240 if ($this->original['resolution'] != $this->modified['resolution'])
241 {
242 $this->notice_resolution_change($this->original['resolution'], $this->modified['resolution']);
243 }
244 if ($this->original['duplicates'] != $this->modified['duplicates'])
245 {
246 $this->notice_duplicates_change($this->original['duplicates'], $this->modified['duplicates']);
247 }
248
249 // other standard fields that don't have custom masks
250 if ($this->original['severity'] != $this->modified['severity'])
251 {
252 $this->notice_severity_change($this->original['severity'], $this->modified['severity']);
253 }
254 if ($this->original['priority'] != $this->modified['priority'])
255 {
256 $this->notice_priority_change($this->original['priority'], $this->modified['priority']);
257 }
258 if (($this->original['product'] != $this->modified['product']) OR ($this->original['component'] != $this->modified['component']) OR ($this->original['version'] != $this->modified['version']))
259 {
260 $this->notice_pcv_change(array($this->original['product'], $this->original['component'], $this->original['version']), array($this->modified['product'], $this->modified['component'], $this->modified['version']));
261 }
262
263 $dofields = array(
264 'summary' => -1,
265 'dependency' => -1,
266 'hidden' => -1
267 );
268 foreach ($dofields AS $field => $lookup)
269 {
270 if ($this->original["$field"] != $this->modified["$field"])
271 {
272 $this->notice_other_change($field, $this->original["$field"], $this->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 $user = construct_user_display($this->registry->userinfo, false);
291 $this->notices["$userid"][] = eval('$part = "' . $this->registry->template->fetch('email/notice_unassigned.part') . '";');
292 }
293 }
294
295 // ###################################################################
296 /**
297 * Informs the user that they have been made the assignee of the bug.
298 *
299 * @access private
300 *
301 * @param integer User ID
302 */
303 function notice_now_assigned($userid)
304 {
305 if ($this->users["$userid"]['options'][0] & $this->registry->emailoptions['notifications']['assignedto'] AND in_array($userid, $this->roles['-notapplicable-']))
306 {
307 $user = construct_user_display($this->registry->userinfo, false);
308 $this->notices["$userid"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_assigned.part') . '";');
309 }
310 }
311
312 // ###################################################################
313 /**
314 * Sends a message to inform users that the status has changed.
315 *
316 * @access private
317 *
318 * @param integer Old status
319 * @param integer New status
320 */
321 function notice_status_change($old, $new)
322 {
323 $userlist = $this->fetch_users_with_on_bit('statusresolve');
324 foreach ($userlist AS $userid => $user)
325 {
326 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_status.part') . '";');
327 }
328 }
329
330 // ###################################################################
331 /**
332 * Sends an email to inform users that the resolution has changed.
333 *
334 * @access private
335 *
336 * @param integer Old resolution
337 * @param integer New resolution
338 */
339 function notice_resolution_change($old, $new)
340 {
341 $userlist = $this->fetch_users_with_on_bit('statusresolve');
342 foreach ($userlist AS $userid => $user)
343 {
344 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_resolution.part') . '";');
345 }
346 }
347
348 // ###################################################################
349 /**
350 * Informs users that the duplicates list has changed.
351 *
352 * @access private
353 *
354 * @param string Old duplicates list
355 * @param string New duplicates list
356 */
357 function notice_duplicates_change($old, $new)
358 {
359 $userlist = $this->fetch_useres_with_on_bit('duplicates');
360 foreach ($userlist AS $userid => $user)
361 {
362 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_duplicates.part') . '";');
363 }
364 }
365
366 // ###################################################################
367 /**
368 * Sends an email to inform users that the severity has changed.
369 *
370 * @access private
371 *
372 * @param integer Old severity
373 * @param integer New severity
374 */
375 function notice_severity_change($old, $new)
376 {
377 $userlist = $this->fetch_users_with_on_bit('otherfield');
378 foreach ($userlist AS $userid => $user)
379 {
380 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_severity.part') . '";');
381 }
382 }
383
384 // ###################################################################
385 /**
386 * Informs users that the priority changed.
387 *
388 * @access private
389 *
390 * @param integer Old priority
391 * @param integer New priority
392 */
393 function notice_priority_change($old, $new)
394 {
395 $userlist = $this->fetch_users_with_on_bit('otherfield');
396 foreach ($userlist AS $userid => $user)
397 {
398 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_priority.part') . '";');
399 }
400 }
401
402 // ###################################################################
403 /**
404 * Sends an email telling users that the product, component, or version
405 * has changed. This is done all at once because you really need to see
406 * the whole thing in the notice.
407 *
408 * @access private
409 *
410 * @param array Original PCV
411 * @param array Modified PCV
412 */
413 function notice_pcv_change($old, $new)
414 {
415 $userlist = $this->fetch_users_with_on_bit('otherfield');
416
417 $old = $this->registry->datastore['product']["$old[0]"]['title'] . '/' . ($old[1] ? $this->registry->datastore['product']["$old[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$old[2]"]['version'];
418 $new = $this->registry->datastore['product']["$new[0]"]['title'] . '/' . ($new[1] ? $this->registry->datastore['product']["$new[1]"]['title'] . '/' : '') . $this->registry->datastore['version']["$new[2]"]['version'];
419
420 foreach ($userlist AS $userid => $user)
421 {
422 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_product.part') . '";');
423 }
424 }
425
426 // ###################################################################
427 /**
428 * Sends the appropriate users information about a new comment being
429 * posted to the bug report.
430 *
431 * @access public
432 *
433 * @param array CommentAPI->values array
434 */
435 function send_new_comment_notice($comment)
436 {
437 $userlist = $this->fetch_users_with_on_bit('newcomment');
438 foreach ($userlist AS $userid => $user)
439 {
440 $user = construct_user_display($this->registry->userinfo, false);
441 $date = $this->registry->modules['date']->format($this->registry->options['dateformat'], $comment['dateline']);
442
443 $this->notices["$userid"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_comment.part') . '";');
444 }
445 }
446
447 // ###################################################################
448 /**
449 * A notice for an individual field changing.
450 *
451 * @access private
452 *
453 * @param string Field name
454 * @param mixed Original value
455 * @param mixed Modified value
456 */
457 function notice_other_change($name, $old, $new)
458 {
459 $userlist = $this->fetch_users_with_on_bit('otherfield');
460 foreach ($userlist AS $userid => $user)
461 {
462 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_other.part') . '";');
463 }
464 }
465
466 // ###################################################################
467 /**
468 * Sends appropriate users a notice when a new attachment has been
469 * added.
470 *
471 * @access public
472 *
473 * @param array AttachmentAPI->values array
474 * @param array List of all attachments made obsolete
475 * @param array Newly-inserted attachment ID
476 */
477 function send_new_attachment_notice($attachment, $obsolete, $id)
478 {
479 $userlist = $this->fetch_users_with_on_bit('newattachment');
480 foreach ($userlist AS $userid => $user)
481 {
482 $user = construct_user_display($this->registry->userinfo, false);
483 $obsoletes = implode(', ', (array)$obsolete);
484
485 $this->notices["$userid"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_attachment.part') . '";');
486 }
487 }
488
489 // ###################################################################
490 /**
491 * Sends a new bug notification notice to all those who have the option
492 * turned no. This does not use fetch_users_with_on_bit() because a
493 * query is more effective.
494 *
495 * @access public
496 *
497 * @param array Bug values array
498 * @param array Comment values array
499 */
500 function send_new_bug_notice($bug, $comment)
501 {
502 $userinfo = $this->registry->db->query("
503 SELECT user.*, useremail.*
504 FROM " . TABLE_PREFIX . "useremail AS useremail
505 LEFT JOIN " . TABLE_PREFIX . "user AS user
506 ON (user.userid = useremail.userid)
507 WHERE useremail.relation = 0
508 AND useremail.mask & " . $this->registry->emailoptions['notifications']['newbug'] . "
509 ");
510 while ($user = $this->registry->db->fetch_array($userinfo))
511 {
512 if (!is_array($this->users["$user[userid]"]))
513 {
514 $user = construct_user_display($this->registry->userinfo, false);
515 $product = $this->registry->datastore['product']["$bug[product]"]['title'] . '/' . ($bug['component'] ? $this->registry->datastore['product']["$bug[component]"]['title'] . '/' : '') . $this->registry->datastore['version']["$bug[version]"]['version'];
516 $this->notices["$user[userid]"][] = eval('$email = "' . $this->registry->template->fetch('email/notice_new_bug.part') . '";');
517 $this->users["$user[userid]"] = $user;
518 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
519 }
520 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
521 }
522 }
523
524 // ###################################################################
525 /**
526 * Generates an array of users who have a given email notification flag
527 * turned on in their bitfields.
528 *
529 * @access private
530 *
531 * @param string Notification bitfield name
532 *
533 * @return array Array of users and their data
534 */
535 function fetch_users_with_on_bit($bitname)
536 {
537 $idlist = array();
538
539 foreach ($this->users AS $user)
540 {
541 foreach ($this->registry->emailoptions['relations'] AS $name => $bit)
542 {
543 if (in_array($user['userid'], $this->roles["$name"]) AND $user['options']["$bit"] & $this->registry->emailoptions['notifications']["$bitname"])
544 {
545 $idlist[] = $user['userid'];
546 }
547 }
548 }
549
550 $masters = array_unique($idlist);
551
552 $return = array();
553 foreach ($masters AS $userid)
554 {
555 $return["$userid"] =& $this->users["$userid"];
556 }
557
558 return $return;
559 }
560
561 // ###################################################################
562 /**
563 * Compiles and sends the actual emails to users.
564 *
565 * @access public
566 */
567 function finalize()
568 {
569 // get the current bug for permissions checks
570 $bug = $this->registry->db->query_first("SELECT * FROM " . TABLE_PREFIX . "bug WHERE bugid = " . $this->bug['bugid']);
571 foreach ($this->notices AS $userid => $noticelist)
572 {
573 if ($userid == $this->registry->userinfo['userid'])
574 {
575 $this->registry->debug("skipping user $userid because they're the one doing the thing");
576 continue;
577 }
578
579 // we wouldn't want people who favorite bugs getting hidden notices
580 if (!check_bug_permissions($bug, $this->users["$userid"]))
581 {
582 $this->registry->debug("skipping user $userid ({$this->users[$userid]['email']}) because of permissions");
583 continue;
584 }
585
586 $parts = implode("\n\n", $noticelist);
587
588 eval('$email = "' . $this->registry->template->fetch('email/bugnotification.xml') . '";');
589 $email = $this->registry->xml->parse($email);
590 $this->registry->mail->setSubject($email['email']['subject']['value']);
591 $this->registry->mail->setBodyText($email['email']['bodyText']['value']);
592
593 if (!empty($this->users["$userid"]['email']))
594 {
595 $this->registry->mail->send($this->users["$userid"]['email'], $this->users["$userid"]['displayname']);
596 }
597 else
598 {
599 $this->registry->debug("not sending an email to " . $userid . " because they don't have one?");
600 }
601 }
602 }
603 }
604
605 /*=====================================================================*\
606 || ###################################################################
607 || # $HeadURL$
608 || # $Id$
609 || ###################################################################
610 \*=====================================================================*/
611 ?>