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