PHP5-ing and refactoring class_notification.php
[bugdar.git] / includes / class_notification.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Bugdar
5 || # Copyright ©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 ©2002 - 2007, Blue Static
30 * @version $Revision$
31 * @package Bugdar
32 *
33 */
34 class NotificationCenter
35 {
36 /**
37 * Bug information
38 * @var array
39 */
40 private $bug = array();
41
42 /**
43 * Original bug data
44 * @var array
45 */
46 private $original = array();
47
48 /**
49 * Modified bug data
50 * @var array
51 */
52 private $modified = array();
53
54 /**
55 * Role list: a list of user IDs with their relations to the bug
56 * @var array
57 */
58 private $roles = array(
59 '-notapplicable-' => array(),
60 'reporter' => array(),
61 'assignee' => array(),
62 'favorite' => array(),
63 'voter' => array(),
64 'commenter' => array()
65 );
66
67 /**
68 * User cache list
69 * @var array
70 */
71 private $users = array();
72
73 /**
74 * A list of notices per-user that are combined together in NotificationCenter::finalize()
75 * @var array
76 */
77 private $notices = array();
78
79 /**
80 * Sets the bug data so that all methods in this class have access to
81 * it when sending emails.
82 *
83 * @param array Original bug data
84 * @param array Modified bug data
85 */
86 public function setBugData($original, $modified = array())
87 {
88 if (sizeof($modified) > 0)
89 {
90 $this->bug = $modified;
91 }
92 else
93 {
94 $this->bug = $original;
95 }
96
97 $this->original = $original;
98 $this->modified = $modified;
99
100 $this->roles['-notapplicable-'] = (sizeof($modified) > 0 ? array($original['assignedto'], $modified['assignedto']) : array($original['assignedto']));
101 $this->roles['reporter'] = array($original['userid']);
102 $this->roles['assignee'][] = (sizeof($modified) > 0 ? $modified['assignedto'] : $original['assignedto']);
103
104 $this->_fetchUserCache();
105 }
106
107 /**
108 * Fetches all the users who could be related to the bug and sticks
109 * their information into an array.
110 */
111 private function _fetchUserCache()
112 {
113 $newbuggers = BSApp::$db->query("SELECT userid FROM " . TABLE_PREFIX . "useremail WHERE relation = " . bugdar::$emailOptions['relations']['-notapplicable-'] . " AND mask & " . bugdar::$emailOptions['notifications']['newbug']);
114 foreach ($newbuggers as $newbug)
115 {
116 $this->roles['-notapplicable-']["$newbug[userid]"] = $newbug['userid'];
117 }
118
119 $favorites = BSApp::$db->query("SELECT userid FROM " . TABLE_PREFIX . "favorite WHERE bugid = " . BSApp::$input->clean($this->bug['bugid'], TYPE_UINT));
120 foreach ($favorites as $fav)
121 {
122 $this->roles['favorite']["$fav[userid]"] = $fav['userid'];
123 }
124
125 $voters = BSApp::$db->queryFirst("SELECT userids FROM " . TABLE_PREFIX . "vote WHERE bugid = " . BSApp::$input->clean($this->bug['bugid'], TYPE_UINT));
126 $this->roles['voter'] = preg_split('#,#', $voters['userids'], 0, PREG_SPLIT_NO_EMPTY);
127
128 $commenters = BSApp::$db->query("SELECT userid FROM " . TABLE_PREFIX . "comment WHERE bugid = " . BSApp::$input->clean($this->bug['bugid'], TYPE_UINT));
129 foreach ($commenters as $comment)
130 {
131 $this->roles['commenter']["$comment[userid]"] = $comment['userid'];
132 }
133
134 $masterids = array_merge($this->roles['-notapplicable-'], $this->roles['reporter'], $this->roles['assignee'], $this->roles['favorite'], $this->roles['voter'], $this->roles['commenter']);
135 $masterids = BSFunctions::array_strip_empty(array_unique($masterids));
136
137 if (is_array($masterids) && sizeof($masterids) > 0)
138 {
139 $userinfo = BSApp::$db->query("
140 SELECT user.*, useremail.*
141 FROM " . TABLE_PREFIX . "useremail AS useremail
142 LEFT JOIN " . TABLE_PREFIX . "user AS user
143 ON (user.userid = useremail.userid)
144 WHERE useremail.userid IN (" . implode(',', $masterids) . ")
145 ");
146 foreach ($userinfo as $user)
147 {
148 if (!is_array($this->users["$user[userid]"]))
149 {
150 $this->users["$user[userid]"] = $user;
151 unset($this->users["$user[userid]"]['mask'], $this->users["$user[userid]"]['relation']);
152 }
153 $this->users["$user[userid]"]['options']["$user[relation]"] = $user['mask'];
154 }
155 }
156 }
157
158 /**
159 * Sends the appropriate emails for changes to bugs. This function
160 * works a lot like the Logging class by taking BugAPI->record and
161 * BugAPI->values and then comparing the two arries and sending emails
162 * with the differences.
163 */
164 public function sendBugChangeNotice()
165 {
166 if (!isset($this->modified['bugid']))
167 {
168 return;
169 }
170
171 // fields with custom mask information
172 if ($this->original['assignedto'] != $this->modified['assignedto'])
173 {
174 if ($this->original['assignedto'] != '')
175 {
176 $this->_noticeNoLongerAssigned($this->original['assignedto']);
177 }
178 if ($this->modified['assignedto'] != '')
179 {
180 $this->_noticeNowAssigned($this->modified['assignedto']);
181 }
182 }
183 if ($this->original['status'] != $this->modified['status'])
184 {
185 $this->_noticeStatusChange($this->original['status'], $this->modified['status']);
186 }
187 if ($this->original['resolution'] != $this->modified['resolution'])
188 {
189 $this->_noticeResolutionChange($this->original['resolution'], $this->modified['resolution']);
190 }
191 if ($this->original['duplicates'] != $this->modified['duplicates'])
192 {
193 $this->_noticeDuplicatesChange($this->original['duplicates'], $this->modified['duplicates']);
194 }
195
196 // other standard fields that don't have custom masks
197 if ($this->original['severity'] != $this->modified['severity'])
198 {
199 $this->_noticeSeverityChange($this->original['severity'], $this->modified['severity']);
200 }
201 if ($this->original['priority'] != $this->modified['priority'])
202 {
203 $this->_noticePriorityChange($this->original['priority'], $this->modified['priority']);
204 }
205 if (($this->original['product'] != $this->modified['product']) || ($this->original['component'] != $this->modified['component']) || ($this->original['version'] != $this->modified['version']))
206 {
207 $this->_noticePCVChange(array($this->original['product'], $this->original['component'], $this->original['version']), array($this->modified['product'], $this->modified['component'], $this->modified['version']));
208 }
209
210 $dofields = array(
211 'summary' => -1,
212 'dependency' => -1,
213 'hidden' => -1
214 );
215 foreach ($dofields as $field => $lookup)
216 {
217 if ($this->original["$field"] != $this->modified["$field"])
218 {
219 $this->_noticeOtherChange($field, $this->original["$field"], $this->modified["$field"]);
220 }
221 }
222 }
223
224 /**
225 * Sends an email to the specified user ID that they are no longer the
226 * person assigned to the bug.
227 *
228 * @param integer User ID to send to
229 */
230 private function _noticeNoLongerAssigned($userid)
231 {
232 if ($this->users["$userid"]['options'][0] & bugdar::$emailOptions['notifications']['assignedto'] && in_array($userid, $this->roles['-notapplicable-']))
233 {
234 $user = construct_user_display(bugdar::$userinfo, false);
235 eval('$part = "' . $this->registry->template->fetch(FetchEmailPath('notice_unassigned.part', $this->_localeFromUserId($userid))) . '";');
236 $this->notices["$userid"][] = $part;
237 }
238 }
239
240 /**
241 * Informs the user that they have been made the assignee of the bug.
242 *
243 * @param integer User ID
244 */
245 private function _noticeNowAssigned($userid)
246 {
247 if ($this->users["$userid"]['options'][0] & bugdar::$emailOptions['notifications']['assignedto'] && in_array($userid, $this->roles['-notapplicable-']))
248 {
249 $user = construct_user_display(bugdar::$userinfo, false);
250 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_assigned.part', $this->_localeFromUserId($userid))) . '";');
251 $this->notices["$userid"][] = $email;
252 }
253 }
254
255 /**
256 * Sends a message to inform users that the status has changed.
257 *
258 * @param integer Old status
259 * @param integer New status
260 */
261 private function _noticeStatusChange($old, $new)
262 {
263 $userlist = $this->_fetchUsersWithOnBit('statusresolve');
264 foreach ($userlist as $userid => $user)
265 {
266 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_status.part', $this->_localeFromUserId($userid))) . '";');
267 $this->notices["$user[userid]"][] = $email;
268 }
269 }
270
271 /**
272 * Sends an email to inform users that the resolution has changed.
273 *
274 * @param integer Old resolution
275 * @param integer New resolution
276 */
277 private function _noticeResolutionChange($old, $new)
278 {
279 $userlist = $this->_fetchUsersWithOnBit('statusresolve');
280 foreach ($userlist as $userid => $user)
281 {
282 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_resolution.part', $this->_localeFromUserId($userid))) . '";');
283 $this->notices["$user[userid]"][] = $email;
284 }
285 }
286
287 /**
288 * Informs users that the duplicates list has changed.
289 *
290 * @param string Old duplicates list
291 * @param string New duplicates list
292 */
293 private function _noticeDuplicatesChange($old, $new)
294 {
295 $userlist = $this->_fetchUsersWithOnBit('duplicates');
296 foreach ($userlist as $userid => $user)
297 {
298 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_duplicates.part', $this->_localeFromUserId($userid))) . '";');
299 $this->notices["$user[userid]"][] = $email;
300 }
301 }
302
303 /**
304 * Sends an email to inform users that the severity has changed.
305 *
306 * @param integer Old severity
307 * @param integer New severity
308 */
309 private function _noticeSeverityChange($old, $new)
310 {
311 $userlist = $this->_fetchUsersWithOnBit('otherfield');
312 foreach ($userlist as $userid => $user)
313 {
314 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_severity.part', $this->_localeFromUserId($userid))) . '";');
315 $this->notices["$user[userid]"][] = $email;
316 }
317 }
318
319 /**
320 * Informs users that the priority changed.
321 *
322 * @param integer Old priority
323 * @param integer New priority
324 */
325 private function _noticePriorityChange($old, $new)
326 {
327 $userlist = $this->_fetchUsersWithOnBit('otherfield');
328 foreach ($userlist as $userid => $user)
329 {
330 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_priority.part', $this->_localeFromUserId($userid))) . '";');
331 $this->notices["$user[userid]"][] = $email;
332 }
333 }
334
335 /**
336 * Sends an email telling users that the product, component, or version
337 * has changed. This is done all at once because you really need to see
338 * the whole thing in the notice.
339 *
340 * @param array Original PCV
341 * @param array Modified PCV
342 */
343 private function _noticePCVChange($old, $new)
344 {
345 $userlist = $this->_fetchUsersWithOnBit('otherfield');
346
347 $old = bugdar::$datastore['product']["$old[0]"]['title'] . '/' . ($old[1] ? bugdar::$datastore['product']["$old[1]"]['title'] . '/' : '') . bugdar::$datastore['version']["$old[2]"]['version'];
348 $new = bugdar::$datastore['product']["$new[0]"]['title'] . '/' . ($new[1] ? bugdar::$datastore['product']["$new[1]"]['title'] . '/' : '') . bugdar::$datastore['version']["$new[2]"]['version'];
349
350 foreach ($userlist as $userid => $user)
351 {
352 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_product.part', $this->_localeFromUserId($userid))) . '";');
353 $this->notices["$user[userid]"][] = $email;
354 }
355 }
356
357 /**
358 * Sends the appropriate users information about a new comment being
359 * posted to the bug report.
360 *
361 * @param array CommentAPI->values array
362 */
363 public function sendNewCommentNotice($comment)
364 {
365 $userlist = $this->_fetchUsersWithOnBit('newcomment');
366 foreach ($userlist as $userid => $user)
367 {
368 $user = construct_user_display(bugdar::$userinfo, false);
369 $date = $this->registry->modules['date']->format(bugdar::$options['dateformat'], $comment['dateline']);
370
371 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_comment.part', $this->_localeFromUserId($userid))) . '";');
372 $this->notices["$userid"][] = $email;
373 }
374 }
375
376 /**
377 * A notice for an individual field changing.
378 *
379 * @param string Field name
380 * @param mixed Original value
381 * @param mixed Modified value
382 */
383 private function _noticeOtherChange($name, $old, $new)
384 {
385 $userlist = $this->_fetchUsersWithOnBit('otherfield');
386 foreach ($userlist as $userid => $user)
387 {
388 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_other.part', $this->_localeFromUserId($userid))) . '";');
389 $this->notices["$user[userid]"][] = $email;
390 }
391 }
392
393 /**
394 * Sends appropriate users a notice when a new attachment has been
395 * added.
396 *
397 * @param array AttachmentAPI->values array
398 * @param array List of all attachments made obsolete
399 * @param array Newly-inserted attachment ID
400 */
401 public function sendNewAttachmentNotice($attachment, $obsolete, $id)
402 {
403 $userlist = $this->_fetchUsersWithOnBit('newattachment');
404 foreach ($userlist as $userid => $user)
405 {
406 $user = construct_user_display(bugdar::$userinfo, false);
407 $obsoletes = implode(', ', (array)$obsolete);
408
409 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_attachment.part', $this->_localeFromUserId($userid))) . '";');
410 $this->notices["$userid"][] = $email;
411 }
412 }
413
414 /**
415 * Sends a new bug notification notice to all those who have the option
416 * turned no. This does not use fetchUsersWithOnBit() because a
417 * query is more effective.
418 *
419 * @param array Bug values array
420 * @param array Comment values array
421 */
422 public function sendNewBugNotice($bug, $comment)
423 {
424 $userinfo = BSApp::$db->query("
425 SELECT user.*, useremail.*
426 FROM " . TABLE_PREFIX . "useremail AS useremail
427 LEFT JOIN " . TABLE_PREFIX . "user AS user
428 ON (user.userid = useremail.userid)
429 WHERE useremail.relation = 0
430 AND useremail.mask & " . bugdar::$emailOptions['notifications']['newbug'] . "
431 ");
432 foreach ($userinfo as $userInfo)
433 {
434 if (!is_array($this->users["$userInfo[userid]"]))
435 {
436 $user = construct_user_display(bugdar::$userinfo, false);
437 $this->users["$userInfo[userid]"] = $userInfo;
438 $product = bugdar::$datastore['product']["$bug[product]"]['title'] . '/' . ($bug['component'] ? bugdar::$datastore['product']["$bug[component]"]['title'] . '/' : '') . bugdar::$datastore['version']["$bug[version]"]['version'];
439 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('notice_new_bug.part', $this->_localeFromUserId($userInfo['userid']))) . '";');
440 $this->notices["$userInfo[userid]"][] = $email;
441 unset($this->users["$userInfo[userid]"]['mask'], $this->users["$userInfo[userid]"]['relation']);
442 }
443 $this->users["$userInfo[userid]"]['options']["$userInfo[relation]"] = $userInfo['mask'];
444 }
445 }
446
447 /**
448 * Generates an array of users who have a given email notification flag
449 * turned on in their bitfields.
450 *
451 * @param string Notification bitfield name
452 *
453 * @return array Array of users and their data
454 */
455 private function _fetchUsersWithOnBit($bitname)
456 {
457 $idlist = array();
458
459 foreach ($this->users as $user)
460 {
461 foreach (bugdar::$emailOptions['relations'] as $name => $bit)
462 {
463 if (in_array($user['userid'], $this->roles["$name"]) && $user['options']["$bit"] & bugdar::$emailOptions['notifications']["$bitname"])
464 {
465 $idlist[] = $user['userid'];
466 }
467 }
468 }
469
470 $masters = array_unique($idlist);
471
472 $return = array();
473 foreach ($masters as $userid)
474 {
475 $return["$userid"] =& $this->users["$userid"];
476 }
477
478 return $return;
479 }
480
481 /**
482 * Compiles and sends the actual emails to users.
483 */
484 public function finalize()
485 {
486 // get the current bug for permissions checks
487 $bug = BSApp::$db->query_first("SELECT * FROM " . TABLE_PREFIX . "bug WHERE bugid = " . $this->bug['bugid']);
488 foreach ($this->_notices as $userid => $noticelist)
489 {
490 if ($userid == bugdar::$userinfo['userid'])
491 {
492 BSApp::debug("skipping user $userid because they're the one doing the thing");
493 continue;
494 }
495
496 // we wouldn't want people who favorite bugs getting hidden notices
497 if (!check_bug_permissions($bug, $this->users["$userid"]))
498 {
499 BSApp::debug("skipping user $userid ({$this->users[$userid]['email']}) because of permissions");
500 continue;
501 }
502
503 $parts = implode("\n\n", $noticelist);
504
505 eval('$email = "' . $this->registry->template->fetch(FetchEmailPath('bugnotification.xml', $this->_localeFromUserId($userid))) . '";');
506 $email = $this->registry->xml->parse($email, true);
507 $this->registry->mail->setSubject($email['email']['subject']['value']);
508 $this->registry->mail->setBodyText($email['email']['bodyText']['value']);
509
510 if (!empty($this->users["$userid"]['email']))
511 {
512 $this->registry->mail->send($this->users["$userid"]['email'], $this->users["$userid"]['displayname']);
513 }
514 else
515 {
516 BSApp::debug("not sending an email to " . $userid . " because they don't have one?");
517 }
518 }
519 }
520
521 /**
522 * Returns the locale name from a given user ID
523 *
524 * @param integer User ID
525 *
526 * @return string Locale
527 */
528 private function _localeFromUserId($userid)
529 {
530 $langcode = bugdar::$datastore['language'][$this->users[$userid]['languageid']]['langcode'];
531 if (!$langcode)
532 {
533 $langcode = bugdar::$datastore['language'][bugdar::$options['defaultlanguage']]['langcode'];
534 }
535 return $langcode;
536 }
537 }
538
539 /*=====================================================================*\
540 || ###################################################################
541 || # $HeadURL$
542 || # $Id$
543 || ###################################################################
544 \*=====================================================================*/
545 ?>