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