php8
[web.mtrack] / MTrack / Issue.php
1 <?php
2
3 throw new Exception("disabled");
4
5 require_once 'MTrack/Interface/IssueListener.php';
6 require_once 'MTrack/DB.php';
7 require_once 'MTrack/Config.php';
8 require_once 'MTrack/Component.php';
9 require_once 'MTrack/Wiki.php';
10 //require_once 'MTrack/Changeset.php';
11 require_once 'MTrack/SearchDB.php';
12 require_once 'MTrack/Milestone.php';
13 require_once 'MTrack/Keyword.php';
14
15
16 class MTrackIssue {
17   public $tid = null;
18   public $nsident = null;
19   public $summary = null;
20   public $description = null;
21   public $created = null;
22   public $updated = null;
23   public $owner = null;
24   public $priority = null;
25   public $severity = null;
26   public $classification = null;
27   public $resolution = null;
28   public $status = null;
29   public $estimated = null;
30   public $spent = null;
31   public $changelog = null;
32   public $cc = null;
33   protected $components = null;
34   protected $origcomponents = null;
35   protected $milestones = null;
36   protected $origmilestones = null;
37   protected $comments_to_add = array();
38   protected $keywords = null;
39   protected $origkeywords = null;
40   protected $effort = array();
41
42   static $_listeners = array();
43   static function loadById($id) {
44     try {
45       return new MTrackIssue($id);
46     } catch (Exception $e) {
47     }
48     return null;
49   }
50   static function loadByNSIdent($id) // really the integer id.
51   {
52     static $cache = array();
53     if (!isset($cache[$id])) {
54       $ids = MTrackDB::q('select tid from tickets where nsident = ?', $id)
55             ->fetchAll(PDO::FETCH_COLUMN, 0);
56       if (count($ids) == 1) {
57         $cache[$id] = $ids[0];
58       } else {
59         return null;
60       }
61     }
62     return new MTrackIssue($cache[$id]);
63   }
64   static function registerListener(IMTrackIssueListener $l) // used by CustomFields
65   {
66     self::$_listeners[] = $l;
67   }
68   static function index_issue($object)
69   {
70     list($ignore, $ident) = explode(':', $object, 2);
71     $i = MTrackIssue::loadById($ident);
72     if (!$i) return;
73     echo "Ticket #$i->nsident\n";
74
75     $CS = MTrackChangeset::get($i->updated);
76     $CSC = MTrackChangeset::get($i->created);
77
78     $kw = join(' ', array_values($i->getKeywords()));
79     $idx = array(
80             'summary' => $i->summary,
81             'description' => $i->description,
82             'changelog' => $i->changelog,
83             'keyword' => $kw,
84             'stored:date' => $CS->when,
85             'who' => $CS->who,
86             'creator' => $CSC->who,
87             'stored:created' => $CSC->when,
88             'owner' => $i->owner
89             );
90     $i->augmentIndexerFields($idx);
91     MTrackSearchDB::add("ticket:$i->tid", $idx, true);
92
93     foreach (MTrackDB::q('select value, changedate, who from
94         change_audit left join changes using (cid) where fieldname = ?',
95         "ticket:$ident:@comment") as $row) {
96       list($text, $when, $who) = $row;
97       $start = time();
98       $id = sha1($text);
99       $elapsed = time() - $start;
100       if ($elapsed > 4) {
101         echo "  - comment $who $when took $elapsed to hash\n";
102       }
103       $start = time();
104       if (strlen($text) > 8192) {
105         // A huge paste into a ticket
106         $text = substr($text, 0, 8192);
107       }
108       MTrackSearchDB::add("ticket:$ident:comment:$id", array(
109         'description' => $text,
110         'stored:date' => $when,
111         'who' => $who,
112       ), true);
113
114       $elapsed = time() - $start;
115       if ($elapsed > 4) {
116         echo "  - comment $who $when took $elapsed to index\n";
117       }
118     }
119   }
120
121   //methods..
122   
123     function __construct($tid = null) {
124         if ($tid === null) {
125             $this->components = array();
126             $this->origcomponents = array();
127             $this->milestones = array();
128             $this->origmilestones = array();
129             $this->keywords = array();
130             $this->origkeywords = array();
131             $this->status = 'new';
132
133             foreach (array('classification', 'severity', 'priority') as $f) {
134                 $this->$f = MTrackConfig::get('ticket', "default.$f");
135             }
136         } else {
137             $data =  MTrackDB::q('select * from tickets where tid = ?',
138                               $tid)->fetchAll();
139             
140             $row = null;
141             if (isset($data[0])) {
142                 $row = $data[0];
143             } 
144             
145             if (!is_array($row)) {
146                 throw new Exception("no such issue $tid");
147             }
148             
149             foreach ($row as $k => $v) {
150                 $this->$k = $v;
151             }
152         }
153     }
154
155   function applyPOSTData($data) {
156     foreach (self::$_listeners as $l) {
157       $l->applyPOSTData($this, $data);
158     }
159   }
160
161   function augmentFormFields(&$FIELDSET) {
162     foreach (self::$_listeners as $l) {
163       $l->augmentFormFields($this, $FIELDSET);
164     }
165   }
166   function augmentIndexerFields(&$idx) {
167     foreach (self::$_listeners as $l) {
168       $l->augmentIndexerFields($this, $idx);
169     }
170   }
171   function augmentSaveParams(&$params) {
172     foreach (self::$_listeners as $l) {
173       $l->augmentSaveParams($this, $params);
174     }
175   }
176
177   function checkVeto()
178   {
179     $args = func_get_args();
180     $method = array_shift($args);
181     $veto = array();
182
183     foreach (self::$_listeners as $l) {
184       $v = call_user_func_array(array($l, $method), $args);
185       if ($v !== true) {
186         $veto[] = $v;
187       }
188     }
189     if (count($veto)) {
190       $reasons = array();
191       foreach ($veto as $r) {
192         if (is_array($r)) {
193           foreach ($r as $m) {
194             $reasons[] = $m;
195           }
196         } else {
197           $reasons[] = $r;
198         }
199       }
200       require_once 'Exception/Veto.php';
201       throw new MTrackVetoException($reasons);
202     }
203   }
204
205   function save(MTrackChangeset $CS)
206   {
207     $db = MTrackDB::get();
208     $reindex = false;
209
210     if ($this->tid === null) {
211       $this->created = $CS->cid;
212       $oldrow = array();
213       $reindex = true;
214     } else {
215       list($oldrow) = MTrackDB::q('select * from tickets where tid = ?',
216                         $this->tid)->fetchAll();
217     }
218
219     $this->checkVeto('vetoSave', $this, $oldrow);
220
221     $this->updated = $CS->cid;
222
223     $params = array(
224       'summary' => $this->summary,
225       'description' => $this->description,
226       'created' => $this->created,
227       'updated' => $this->updated,
228       'owner' => $this->owner,
229       'changelog' => $this->changelog,
230       'priority' => $this->priority,
231       'severity' => $this->severity,
232       'classification' => $this->classification,
233       'resolution' => $this->resolution,
234       'status' => $this->status,
235       'estimated' => (float)$this->estimated,
236       'spent' => (float)$this->spent,
237       'nsident' => $this->nsident,
238       'cc' => $this->cc,
239     );
240
241     $this->augmentSaveParams($params);
242
243     if ($this->tid === null) {
244       $sql = 'insert into tickets ';
245       $keys = array();
246       $values = array();
247       
248       require_once 'UUID.php';
249       $new_tid = new OmniTI_Util_UUID;
250       $new_tid = $new_tid->toRFC4122String(false);
251
252       $keys[] = "tid";
253       $values[] = "'$new_tid'";
254
255       foreach ($params as $key => $value) {
256         $keys[] = $key;
257         $values[] = ":$key";
258       }
259
260       $sql .= "(" . join(', ', $keys) . ") values (" .
261               join(', ', $values) . ")";
262     } else {
263       $sql = 'update tickets set ';
264       $values = array();
265       foreach ($params as $key => $value) {
266         $values[] = "$key = :$key";
267       }
268       $sql .= join(', ', $values) . " where tid = :tid";
269
270       $params['tid'] = $this->tid;
271     }
272
273     $q = $db->prepare($sql);
274     $q->execute($params);
275
276     if ($this->tid === null) {
277       $this->tid = $new_tid;
278       $created = true;
279     } else {
280       $created = false;
281     }
282
283     foreach ($params as $key => $value) {
284       if ($key == 'created' || $key == 'updated' || $key == 'tid') {
285         continue;
286       }
287       if ($key == 'changelog' || $key == 'description' || $key == 'summary') {
288         if (!isset($oldrow[$key]) || $oldrow[$key] != $value) {
289           $reindex = true;
290         }
291       }
292       if (!isset($oldrow[$key])) {
293         $oldrow[$key] = null;
294       }
295       $CS->add("ticket:$this->tid:$key", $oldrow[$key], $value);
296     }
297
298     $this->compute_diff($CS, 'components', 'ticket_components', 'compid',
299         $this->components, $this->origcomponents);
300     $this->compute_diff($CS, 'keywords', 'ticket_keywords', 'kid',
301         $this->keywords, $this->origkeywords);
302     $this->compute_diff($CS, 'milestones', 'ticket_milestones', 'mid',
303         $this->milestones, $this->origmilestones);
304
305     foreach ($this->comments_to_add as $text) {
306       $CS->add("ticket:$this->tid:@comment", null, $text);
307     }
308
309     foreach ($this->effort as $effort) {
310       MTrackDB::q('insert into effort (tid, cid, expended, remaining)
311         values (?, ?, ?, ?)',
312         $this->tid, $CS->cid, $effort[0], $effort[1]);
313     }
314     $this->effort = array();
315   }
316  
317   function getComponents()
318   {
319     if ($this->components !== null) {
320         return $this->components ;
321     }
322     $q = MTrackDB::q('
323             select tc.compid, name 
324                 from ticket_components tc 
325                     left join components using (compid) 
326                     where tid = ?', $this->tid);
327   
328     $this->origcomponents = array();
329     foreach ($q->fetchAll() as $row) {
330         $this->origcomponents[$row[0]] = $row[1];
331     }
332     $this->components = $this->origcomponents;
333     
334     return $this->components;
335   }
336
337   function assocComponent($comp)
338   {
339     $comp = $this->resolveComponent($comp);
340     $this->getComponents();
341     $this->checkVeto('vetoComponent', $this, $comp, true);
342     $this->components[$comp->compid] = $comp->name;
343   }
344
345   function dissocComponent($comp)
346   {
347     $comp = $this->resolveComponent($comp);
348     $this->getComponents();
349     $this->checkVeto('vetoComponent', $this, $comp, false);
350     unset($this->components[$comp->compid]);
351   }
352
353   function getMilestones()
354   {
355     if ($this->milestones === null) {
356       $comps = MTrackDB::q('select tc.mid, name from ticket_milestones tc left join milestones using (mid) where tid = ? order by duedate, name', $this->tid)->fetchAll();
357       $this->origmilestones = array();
358       foreach ($comps as $row) {
359         $this->origmilestones[$row[0]] = $row[1];
360       }
361       $this->milestones = $this->origmilestones;
362     }
363     return $this->milestones;
364   }
365
366   
367   function assocMilestone($M)
368   {
369     $ms = $this->resolveMilestone($M);
370     if ($ms === null) {
371       throw new Exception("unable to resolve milestone $M");
372     }
373     $this->getMilestones();
374     $this->checkVeto('vetoMilestone', $this, $ms, true);
375     $this->milestones[$ms->mid] = $ms->name;
376   }
377
378   function dissocMilestone($M)
379   {
380     $ms = $this->resolveMilestone($M);
381     if ($ms === null) {
382       throw new Exception("unable to resolve milestone $M");
383     }
384     $this->getMilestones();
385     $this->checkVeto('vetoMilestone', $this, $ms, false);
386     unset($this->milestones[$ms->mid]);
387   }
388
389   function addComment($comment)
390   {
391     $comment = trim($comment);
392     if (strlen($comment)) {
393       $this->checkVeto('vetoComment', $this, $comment);
394       $this->comments_to_add[] = $comment;
395     }
396   }
397
398
399   function assocKeyword($kw)
400   {
401     $kw = $this->resolveKeyword($kw);
402     $this->getKeywords();
403     $this->checkVeto('vetoKeyword', $this, $kw, true);
404     $this->keywords[$kw->kid] = $kw->keyword;
405   }
406
407   function dissocKeyword($kw)
408   {
409     $kw = $this->resolveKeyword($kw);
410     $this->getKeywords();
411     $this->checkVeto('vetoKeyword', $this, $kw, false);
412     unset($this->keywords[$kw->kid]);
413   }
414
415   function getKeywords()
416   {
417     if ($this->keywords === null) {
418       $comps = MTrackDB::q('select tc.kid, keyword from ticket_keywords tc left join keywords using (kid) where tid = ?', $this->tid)->fetchAll();
419       $this->origkeywords = array();
420       foreach ($comps as $row) {
421         $this->origkeywords[$row[0]] = $row[1];
422       }
423       $this->keywords = $this->origkeywords;
424     }
425     return $this->keywords;
426   }
427
428   function addEffort($amount, $revised = null)
429   {
430     $diff = null;
431     if ($revised !== null) {
432       $diff = $revised - $this->estimated;
433       $this->estimated = $revised;
434     }
435     $this->effort[] = array($amount, $diff);
436     $this->spent += $amount;
437   }
438
439   function close()
440   {
441     $this->status = 'closed';
442     $this->addEffort(0, 0);
443   }
444
445   function isOpen()
446   {
447     switch ($this->status) {
448       case 'closed':
449         return false;
450       default:
451         return true;
452     }
453   }
454
455   function reOpen()
456   {
457     $this->status = 'reopened';
458     $this->resolution = null;
459   }
460
461   
462     function toArray() 
463     {
464         $ret = get_object_vars($this);
465        // echo '<PRE>'; print_r($ret);exit;
466         return $ret;
467     }
468   
469   
470   /// rendering tricks
471   
472   
473     function updateWho($link)
474     {
475         return  $link->username(
476                 $this->updated ? 
477                     MTrackChangeset::get($this->updated)->who :
478                     MTrackAuth::whoami()
479         );
480         
481     }
482     function updateWhen($link) 
483     {
484         return  $link->date( $this->updated ? 
485                 MTrackChangeset::get($this->updated)->when :
486                 MTrackDB::unixtime(time()) 
487         );
488     }
489     function createdWho($link)
490     {
491         return  $link->username(
492                 $this->created ? 
493                     MTrackChangeset::get($this->created)->who :
494                     MTrackAuth::whoami()
495         );
496         
497     }
498     function createdWhen($link) 
499     {
500         return  $link->date( $this->created ? 
501                 MTrackChangeset::get($this->created)->when :
502                 MTrackDB::unixtime(time()) 
503         );
504     }
505   
506     function keywordsToHtml() 
507     {
508         $value = array();
509         foreach ($this->getKeywords() as $kw) {
510             $value[] = mtrack_keyword($kw);
511         }
512         return  join(' ', $value);
513     }
514     
515
516     
517     function componentsToHtml() 
518     {
519         $res = array();
520         $value = join(',',array_keys($this->getComponents()));
521         
522         if (!strlen($value)) {
523             return '';
524         }
525         $q = MTrackDB::q(
526               "select name, deleted from components where compid in ($value)");
527         foreach ($q->fetchAll() as $row) {
528             
529             $c = ($row['deleted'] ? '<del>' : '') .
530                 htmlentities($row['name'], ENT_QUOTES, 'utf-8') . 
531                 ($row['deleted'] ? '</del>' : '');
532             $res[] = $c;
533         }
534         
535         return join(", ", $res);
536        
537     }
538     
539     function milestonesToHtml($msurl) 
540     {
541         if (empty($this->milestone_url)) {
542             die("requires issue->milestone_url to be set.");
543         }
544         $res = array();
545         $value = join(',',array_keys($this->getMilestones()));
546         
547         if (!strlen($value)) {
548             return '';
549         }
550         foreach (MTrackDB::q(
551           "select name, completed, deleted from milestones where mid in ($value)")
552           ->fetchAll() as $row) {
553               
554             $row['deleted'] =   strlen($row['completed']) ? 1 : 0;
555                 
556             $c = "<span class='milestone" . 
557                 ($row['deleted'] ?  " completed" : '') . 
558                 "'><a href=\"{$this->milestone_url}" . urlencode($row['name']) . '">' .
559                 htmlentities($row['name'], ENT_QUOTES, 'utf-8') .
560                 "</a></span>";
561             $res[] = $c;
562         }
563         return join(", ", $res);
564     }
565     function descriptionToHtml()
566     {
567         return MTrack_Wiki::format_to_html($this->description);
568     }
569    /* function attachmentsToHtml()
570     {
571         require_once 'Attachment.php';
572         return MTrackAttachment::renderList("ticket:{$this->tid}");
573     }
574     function attachmentsDeleteToHtml() {
575         require_once 'Attachment.php';
576         return MTrackAttachment::renderDeleteList("ticket:{$this->tid}");
577     }
578  */
579     function toIdString() {
580         return "ticket:{$this->tid}";
581             
582     }
583     
584     // watcher related.
585     function watcherButton()
586     {
587         return MTrackWatch::renderWatchUI('ticket', $this->tid);
588     }
589     
590     function watchType() 
591     {
592         return 'ticket';
593     }
594     function watchId()
595     {
596         return $this->tid;
597     }
598     
599     function watchers()
600     {
601         return MTrackWatch::objectWatchers($this);
602     }
603     
604     
605     private function resolveMilestone($ms)
606     {
607         if ($ms instanceof MTrack_Milestone) {
608           return $ms;
609         }
610         if (ctype_digit($ms)) {
611           return MTrack_Milestone::loadById($ms);
612         }
613         return MTrack_Milestone::loadByName($ms);
614     }
615
616   private function resolveKeyword($kw)
617   {
618     if ($kw instanceof MTrackKeyword) {
619       return $kw;
620     }
621     $k = MTrackKeyword::loadByWord($kw);
622     if ($k === null) {
623       if (ctype_digit($kw)) {
624         return MTrackKeyword::loadById($kw);
625       }
626       throw new Exception("unknown keyword $kw");
627     }
628     return $k;
629   }
630   private function compute_diff(MTrackChangeset $CS, $label,
631         $tablename, $keyname, $current, $orig) {
632     if (!is_array($current)) {
633       $current = array();
634     }
635     if (!is_array($orig)) {
636       $orig = array();
637     }
638     $added = array_keys(array_diff_key($current, $orig));
639     $removed = array_keys(array_diff_key($orig, $current));
640
641     $db = MTrackDB::get();
642     $ADD = $db->prepare(
643       "insert into $tablename (tid, $keyname) values (?, ?)");
644     $DEL = $db->prepare(
645       "delete from $tablename where tid = ? AND $keyname = ?");
646     foreach ($added as $key) {
647       $ADD->execute(array($this->tid, $key));
648     }
649     foreach ($removed as $key) {
650       $DEL->execute(array($this->tid, $key));
651     }
652     if (count($added) + count($removed)) {
653       $old = join(',', array_keys($orig));
654       $new = join(',', array_keys($current));
655       $CS->add(
656         "ticket:$this->tid:@$label", $old, $new);
657     }
658   }
659   private function resolveComponent($comp)
660   {
661     if ($comp instanceof MTrackComponent) {
662       return $comp;
663     }
664     if (ctype_digit($comp)) {
665       return MTrackComponent::loadById($comp);
666     }
667     return MTrackComponent::loadByName($comp);
668   }
669 }