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