import
[web.mtrack] / inc / watch.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 class MTrackWatch {
5   static $possible_event_types = array();
6   static $media = array(
7     'email' => 'Email',
8 //    'timline' => 'Timeline'
9   );
10
11   static function registerEventTypes($objecttype, $events) {
12     self::$possible_event_types[$objecttype] = $events;
13   }
14
15   static function getWatchUI($object, $id) {
16     ob_start();
17     self::renderWatchUI($object, $id);
18     $res = ob_get_contents();
19     ob_end_clean();
20     return $res;
21   }
22
23   static function renderWatchUI($object, $id) {
24     $me = mtrack_canon_username(MTrackAuth::whoami());
25     if ($me == 'anonymous' || MTrackAuth::getUserClass() == 'anonymous') {
26       return;
27     }
28
29     global $ABSWEB;
30     $url = $ABSWEB . 'admin/watch.php?' .
31       http_build_query(array('o' => $object, 'i' => $id));
32     $evts = json_encode(self::$possible_event_types[$object]);
33     $media = json_encode(self::$media);
34     $val = new stdclass;
35     foreach (MTrackDB::q('select medium, event from watches where otype = ? and oid = ? and userid = ? and active = 1', $object, $id, $me)->fetchAll() as $row)
36     {
37       $val->{$row['medium']}->{$row['event']} = true;
38     }
39     $val = json_encode($val);
40     echo <<<HTML
41  <button id='watcher-$object-$id' type='button'>Watch</button>
42 <script>
43 $(document).ready(function () {
44   var evts = $evts;
45   var media = $media;
46   var V = $val;
47   $('#watcher-$object-$id').click(function () {
48     var dlg = $('<div title="Watching"/>');
49     var frm = $('<form/>');
50     var tbl = $('<table/>');
51     var tr = $('<tr/>');
52     tr.append('<th>Event</th>');
53     for (var m in media) {
54       var th = $('<th/>');
55       th.text(media[m]);
56       tr.append(th);
57     }
58     tbl.append(tr);
59
60     for (var i in evts) {
61       tr = $('<tr/>');
62       var td = $('<td/>');
63       td.text(evts[i]);
64       tr.append(td);
65
66       for (var m in media) {
67         td = $('<td/>');
68         var cb = $('<input type="checkbox"/>');
69         if (V[m] && V[m][i]) {
70           cb.attr('checked', 'checked');
71         }
72         cb.data('medium', m);
73         cb.data('event', i);
74         td.append(cb);
75         tr.append(td);
76       }
77       tbl.append(tr);
78     }
79     frm.append(tbl);
80     dlg.append(frm);
81     dlg.dialog({
82       autoOpen: true,
83       bgiframe: true,
84       resizable: false,
85       width: 600,
86       modal: true,
87       buttons: {
88         'Ok': function() {
89           V = {};
90           $("input[type='checkbox'][checked]", dlg).each(function () {
91             var m = $(this).data('medium');
92             var e = $(this).data('event');
93             if (!V[m]) {
94               V[m] = {};
95             }
96             V[m][e] = true;
97           });
98           $.post('$url', {w: JSON.stringify(V)});
99           $(this).dialog('close');
100           dlg.remove();
101         }
102       }
103     });
104   });
105 });
106 </script>
107 HTML;
108   }
109
110   /* Returns an array, keyed by watching entity, of objects that changed
111    * since the specified date.
112    * $watcher = null means all watchers, otherwise specifies the only watcher of interest.
113    * $medium specifies timeline or email (or some other medium)
114    */
115   static function getWatchedItemsAndWatchers($since, $medium, $watcher = null) {
116     if ($watcher) {
117       $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ? and userid = ?', $medium, $watcher);
118     } else {
119       $q = MTrackDB::q('select otype, oid, userid, event from watches where active = 1 and medium = ?', $medium);
120     }
121     $watches = $q->fetchAll(PDO::FETCH_ASSOC);
122
123     $last = strtotime($since);
124     $LATEST = $last;
125
126     $db = MTrackDB::get();
127     $changes = MTrackDB::q(
128       "select * from changes where changedate > ? order by changedate asc",
129       MTrackDB::unixtime($last))->fetchAll(PDO::FETCH_OBJ);
130     $cids = array();
131     $cs_by_cid = array();
132     $by_object = array();
133     foreach ($changes as $CS) {
134       $cids[] = $CS->cid;
135       $cs_by_cid[$CS->cid] = $CS;
136       $t = strtotime($CS->changedate);
137       if ($t > $LATEST) {
138         $LATEST = $t;
139       }
140
141       list($object, $id) = explode(':', $CS->object, 3);
142       $by_object[$object][$id][] = $CS->cid;
143     }
144
145     $repo_by_id = array();
146     $changesets_by_repo_and_rev = array();
147     $related_projects = array();
148
149     foreach (MTrackDB::q('select repoid from repos')
150         ->fetchAll(PDO::FETCH_COLUMN, 0) as $repoid) {
151       $repo = MTrackRepo::loadById($repoid);
152       $repo_by_id[$repoid] = $repo;
153
154       foreach ($repo->history(null, MTrackDB::unixtime($last)) as $e) {
155         /* SCM doesn't always respect our date range */
156         $t = strtotime($e->ctime);
157         if ($t <= $last) {
158           continue;
159         }
160         if ($t > $LATEST) {
161           $LATEST = $t;
162         }
163
164         $key = $repo->getBrowseRootName() . ',' . $e->rev;
165         $e->repo = $repo;
166         $changesets_by_repo_and_rev[$key] = $e;
167
168         $e->_related = array();
169
170         /* relationships to projects based on path */
171         $projid = $repo->projectFromPath($e->files);
172         if ($projid !== null) {
173           $e->_related[] = array('project', $projid);
174           $related_projects[$projid] = $projid;
175         }
176       }
177     }
178
179     /* Ensure that changesets are sorted chronologically */
180     uasort($changesets_by_repo_and_rev, array('MTrackWatch', '_compare_cs'));
181
182     /* Look at the changed tickets: match the reason back to one of the
183      * above changesets */
184     if (isset($by_object['ticket'])) {
185       foreach ($by_object['ticket'] as $tid => $cslist) {
186         foreach ($cslist as $cid) {
187           $CS = $cs_by_cid[$cid];
188           if (!preg_match_all(
189                 "/\(In \[changeset:(([^,]+),([a-zA-Z0-9]+))\]\)/sm",
190                 $CS->reason, $CSM)) {
191             continue;
192           }
193           // $CSM[2] => repo
194           // $CSM[3] => changeset
195           foreach ($CSM[2] as $csm => $csm_repo) {
196             $csm_rev = $CSM[3][$csm];
197
198             /* Look for the repo changeset */
199             $key = "$csm_repo,$csm_rev";
200             if (isset($changesets_by_repo_and_rev[$key])) {
201               $e = $changesets_by_repo_and_rev[$key];
202               $e->CS = $CS;
203               $CS->ent = $e;
204             }
205           }
206         }
207       }
208     }
209
210     $tkt_list = array();
211     $proj_by_tid = array();
212     $emails_by_tid = array();
213     $emails_by_pid = array();
214     $owners_by_csid = array();
215     $milestones_by_tid = array();
216     $milestones_by_cid = array();
217
218     /* determine linked projects and group emails */
219     if (count($related_projects)) {
220       $projlist = join(',', $related_projects);
221       foreach (MTrackDB::q(
222           "select projid, notifyemail from projects where
223           notifyemail is not null and projid in ($projlist)")
224           ->fetchAll(PDO::FETCH_NUM) as $row) {
225         $emails_by_pid[$row[0]] = $row[1];
226       }
227     }
228
229     if (isset($by_object['ticket'])) {
230       $tkt_owner_ids = array();
231       $tkt_cid_list = array();
232       $tkt_milestone_fields = array();
233
234       foreach ($by_object['ticket'] as $tid => $cidlist) {
235         $tkt_list[] = $db->quote($tid);
236         $tkt_owner_ids[] = $db->quote("ticket:$tid:owner");
237         foreach ($cidlist as $cid) {
238           $tkt_cid_list[$cid] = $cid;
239         }
240         /* also want to include folks that were Cc'd */
241         $tkt_owner_ids[] = $db->quote("ticket:$tid:cc");
242         /* milestones */
243         $tkt_milestone_fields[] = $db->quote("ticket:$tid:@milestones");
244       }
245       $tkt_list = join(',', $tkt_list);
246
247       foreach (MTrackDB::q(
248           "select t.tid, p.projid, notifyemail from tickets t left join ticket_components tc on t.tid = tc.tid left join components_by_project cbp on cbp.compid = tc.compid left join projects p on cbp.projid = p.projid where p.projid is not null and t.tid in ($tkt_list)")->fetchAll(PDO::FETCH_NUM) as $row) {
249         $proj_by_tid[$row[0]][$row[1]] = $row[1];
250         if (isset($row[2]) && strlen($row[2])) {
251           $emails_by_tid[$row[0]] = $row[2];
252           $emails_by_pid[$row[1]] = $row[2];
253         }
254       }
255
256       /* determine all changed owners in the affected period */
257       $tkt_owner_ids = join(',', $tkt_owner_ids);
258       $tkt_cid_list = join(',', $tkt_cid_list);
259       foreach (MTrackDB::q(
260           "select cid, oldvalue, value from change_audit where cid in ($tkt_cid_list) and fieldname in ($tkt_owner_ids)")->fetchAll(PDO::FETCH_NUM) as $row) {
261         $cid = array_shift($row);
262         foreach ($row as $owner) {
263           if (!strlen($owner)) continue;
264           $owners_by_csid[$cid][$owner] = mtrack_canon_username($owner);
265         }
266       }
267
268       /* determine all changed milestones in the affected period */
269       $tkt_milestone_fields = join(',', $tkt_milestone_fields);
270       foreach (MTrackDB::q(
271           "select cid, oldvalue, value from change_audit where cid in ($tkt_cid_list) and fieldname in ($tkt_milestone_fields)")->fetchAll(PDO::FETCH_NUM) as $row) {
272         $cid = array_shift($row);
273         foreach ($row as $ms) {
274           $ms = split(',', $ms);
275           foreach ($ms as $mid) {
276             $mid = (int)$mid;
277             $milestones_by_cid[$cid][$mid] = $mid;
278           }
279         }
280       }
281
282       foreach (MTrackDB::q(
283           "select tid, mid from ticket_milestones where tid in ($tkt_list)")
284           ->fetchAll(PDO::FETCH_NUM) as $row) {
285         $milestones_by_tid[$row[0]][$row[1]] = $row[1];
286       }
287     }
288
289     /* walk through list of objects and add related objects */
290     if (isset($by_object['ticket'])) {
291       foreach ($by_object['ticket'] as $tid => $cslist) {
292         foreach ($cslist as $cid) {
293           $CS = $cs_by_cid[$cid];
294           if (!isset($CS->_related)) {
295             $CS->_related = array();
296           }
297
298           if (isset($CS->ent)) {
299             $CS->_related[] = array('repo', $CS->ent->repo->repoid);
300           }
301           if (isset($proj_by_tid[$tid])) {
302             foreach ($proj_by_tid[$tid] as $pid) {
303               $CS->_related[] = array('project', $pid);
304             }
305           }
306           if (isset($milestones_by_tid[$tid])) {
307             foreach ($milestones_by_tid[$tid] as $mid) {
308               $CS->_related[] = array('milestone', $mid);
309             }
310           }
311           if (isset($milestones_by_cid[$cid])) {
312             foreach ($milestones_by_cid[$cid] as $mid) {
313               $CS->_related[] = array('milestone', $mid);
314             }
315           }
316         }
317       }
318     }
319     foreach ($changesets_by_repo_and_rev as $ent) {
320       $ent->_related[] = array('repo', $ent->repo->repoid);
321     }
322
323     /* having determined all changed items, make a pass through to determine
324      * how to associate those with watchers.
325      * Watchers are one of:
326      * - an user with a matching watches entry
327      * - the group email address associated with a project associated with the
328      *   changed object
329      * - the owner of a ticket
330      */
331
332     /* generate synthetic watcher entries for project group emails */
333     foreach ($emails_by_pid as $pid => $email) {
334       $watches[] = array(
335         'otype' => 'project',
336         'oid' => $pid,
337         'userid' => $email,
338         'event' => '*',
339       );
340     }
341
342     foreach ($by_object as $otype => $objects) {
343       foreach ($objects as $oid => $cidlist) {
344         foreach ($cidlist as $cid) {
345           $CS = $cs_by_cid[$cid];
346           if (isset($owners_by_csid[$cid])) {
347             /* add synthetic watcher for a past or current owner */
348             foreach ($owners_by_csid[$cid] as $owner) {
349               $watches[] = array(
350                 'otype' => $otype,
351                 'oid' => $oid,
352                 'userid' => $owner,
353                 'event' => '*'
354               );
355             }
356           }
357           self::_compute_watch($watches, $otype, $oid, $CS);
358           /* eliminate from the set if there are no watchers */
359           if (!isset($CS->_watcher)) {
360             unset($cs_by_cid[$cid]);
361           }
362         }
363       }
364     }
365     foreach ($changesets_by_repo_and_rev as $key => $ent) {
366       self::_compute_watch($watches, 'changeset', $key, $ent);
367       /* eliminate from the set if there are no watchers */
368       if (!isset($ent->_watcher)) {
369         unset($changesets_by_repo_and_rev[$key]);
370       }
371     }
372
373     /* now collect the data by watcher */
374     $by_watcher = array();
375     foreach ($cs_by_cid as $CS) {
376       foreach ($CS->_watcher as $user) {
377         $by_watcher[$user][$CS->cid] = $CS;
378       }
379     }
380     foreach ($changesets_by_repo_and_rev as $key => $ent) {
381       foreach ($ent->_watcher as $user) {
382         /* don't add this if we have an associated CS */
383         if (isset($ent->CS) && $by_watcher[$user][$ent->CS->cid]) {
384           continue;
385         }
386         $by_watcher[$user][$key] = $ent;
387       }
388     }
389     /* one last pass to group the data by object */
390     foreach ($by_watcher as $user => $items) {
391       foreach ($items as $key => $obj) {
392         if ($obj instanceof MTrackSCMEvent) {
393           /* group by repo */
394           $nkey = "repo:" . $obj->repo->repoid;
395         } else {
396           $nkey = $obj->object;
397         }
398         unset($by_watcher[$user][$key]);
399         $by_watcher[$user][$nkey][] = $obj;
400       }
401     }
402
403     return $by_watcher;
404   }
405
406   static function _compute_watch($watches, $otype, $oid, $obj, $event = null) {
407     foreach ($watches as $row) {
408       if ($row['otype'] != $otype) continue;
409       if ($row['oid'] != '*' && $row['oid'] != $oid) continue;
410       if ($event === null || $row['event'] == '*' || $row['event'] == $event) {
411         if (!isset($obj->_watcher)) {
412           $obj->_watcher = array();
413         }
414         $obj->_watcher[$row['userid']] = $row['userid'];
415       }
416     }
417     if ($event === null && isset($obj->_related)) {
418       foreach ($obj->_related as $rel) {
419         self::_compute_watch($watches, $rel[0], $rel[1], $obj, $otype);
420       }
421     }
422   }
423
424   static function _get_project($pid) {
425     static $projects = array();
426     if (isset($projects[$pid])) {
427       return $projects[$pid];
428     }
429     $projects[$pid] = MTrackProject::loadById($pid);
430     return $projects[$pid];
431   }
432
433   /* comparison function for MTrackSCMEvent objects that sorts in ascending
434    * chronological order */
435   static function _compare_cs($A, $B) {
436     return strcmp($A->ctime, $B->ctime);
437   }
438 }
439