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