1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
4 class MTrackEnumeration {
11 public $deleted = null;
14 function enumerate($all = false) {
17 foreach (MTrackDB::q(sprintf("select %s, %s, deleted from %s order by %s",
18 $this->fieldname, $this->fieldvalue, $this->tablename,
20 ->fetchAll(PDO::FETCH_NUM)
22 $res[$row[0]] = array(
25 'deleted' => $row[2] == '1' ? true : false
29 foreach (MTrackDB::q(sprintf("select %s from %s where deleted != '1'",
30 $this->fieldname, $this->tablename))->fetchAll(PDO::FETCH_NUM)
32 $res[$row[0]] = $row[0];
38 function __construct($name = null) {
40 list($row) = MTrackDB::q(sprintf(
41 "select %s, deleted from %s where %s = ?",
42 $this->fieldvalue, $this->tablename, $this->fieldname),
47 $this->value = $row[0];
48 $this->deleted = $row[1];
52 throw new Exception("unable to find $this->tablename with name = $name");
55 $this->deleted = false;
58 function save(MTrackChangeset $CS) {
60 MTrackDB::q(sprintf('insert into %s (%s, %s, deleted) values (?, ?, ?)',
61 $this->tablename, $this->fieldname, $this->fieldvalue),
62 $this->name, $this->value, (int)$this->deleted);
65 list($row) = MTrackDB::q(
66 sprintf('select %s, deleted from %s where %s = ?',
67 $this->fieldname, $this->tablename, $this->fieldvalue),
68 $this->name)->fetchAll();
70 MTrackDB::q(sprintf('update %s set %s = ?, deleted = ? where %s = ?',
71 $this->tablename, $this->fieldvalue, $this->fieldname),
72 $this->value, (int)$this->deleted, $this->name);
74 $CS->add($this->tablename . ":" . $this->name . ":" . $this->fieldvalue,
80 class MTrackTicketState extends MTrackEnumeration {
81 public $tablename = 'ticketstates';
82 protected $fieldname = 'statename';
83 protected $fieldvalue = 'ordinal';
85 static function loadByName($name) {
86 return new MTrackTicketState($name);
91 class MTrackPriority extends MTrackEnumeration {
92 public $tablename = 'priorities';
93 protected $fieldname = 'priorityname';
94 protected $fieldvalue = 'value';
96 static function loadByName($name) {
97 return new MTrackPriority($name);
101 class MTrackSeverity extends MTrackEnumeration {
102 public $tablename = 'severities';
103 protected $fieldname = 'sevname';
104 protected $fieldvalue = 'ordinal';
106 static function loadByName($name) {
107 return new MTrackSeverity($name);
111 class MTrackResolution extends MTrackEnumeration {
112 public $tablename = 'resolutions';
113 protected $fieldname = 'resname';
114 protected $fieldvalue = 'ordinal';
116 static function loadByName($name) {
117 return new MTrackResolution($name);
121 class MTrackClassification extends MTrackEnumeration {
122 public $tablename = 'classifications';
123 protected $fieldname = 'classname';
124 protected $fieldvalue = 'ordinal';
126 static function loadByName($name) {
127 return new MTrackClassification($name);
131 class MTrackComponent {
132 public $compid = null;
134 public $deleted = null;
135 protected $projects = null;
136 protected $origprojects = null;
138 static function loadById($id) {
139 return new MTrackComponent($id);
142 static function loadByName($name) {
143 $rows = MTrackDB::q('select compid from components where name = ?',
144 $name)->fetchAll(PDO::FETCH_COLUMN, 0);
145 if (isset($rows[0])) {
146 return self::loadById($rows[0]);
151 function __construct($id = null) {
153 list($row) = MTrackDB::q(
154 'select name, deleted from components where compid = ?',
156 if (isset($row[0])) {
158 $this->name = $row[0];
159 $this->deleted = $row[1];
162 throw new Exception("unable to find component with id = $id");
164 $this->deleted = false;
167 function getProjects() {
168 if ($this->origprojects === null) {
169 $this->origprojects = array();
170 foreach (MTrackDB::q('select projid from components_by_project where compid = ? order by projid', $this->compid) as $row) {
171 $this->origprojects[] = $row[0];
173 $this->projects = $this->origprojects;
175 return $this->projects;
178 function setProjects($projlist) {
179 $this->projects = $projlist;
182 function save(MTrackChangeset $CS) {
184 list($row) = MTrackDB::q(
185 'select name, deleted from components where compid = ?',
189 'update components set name = ?, deleted = ? where compid = ?',
190 $this->name, (int)$this->deleted, $this->compid);
192 MTrackDB::q('insert into components (name, deleted) values (?, ?)',
193 $this->name, (int)$this->deleted);
194 $this->compid = MTrackDB::lastInsertId('components', 'compid');
197 $CS->add("component:" . $this->compid . ":name", $old['name'], $this->name);
198 $CS->add("component:" . $this->compid . ":deleted", $old['deleted'], $this->deleted);
199 if ($this->projects !== $this->origprojects) {
200 $old = is_array($this->origprojects) ?
201 join(",", $this->origprojects) : '';
202 $new = is_array($this->projects) ?
203 join(",", $this->projects) : '';
204 MTrackDB::q('delete from components_by_project where compid = ?',
206 if (is_array($this->projects)) {
207 foreach ($this->projects as $pid) {
209 'insert into components_by_project (compid, projid) values (?, ?)',
210 $this->compid, $pid);
213 $CS->add("component:$this->compid:projects", $old, $new);
218 class MTrackProject {
219 public $projid = null;
222 public $shortname = null;
223 public $notifyemail = null;
225 static function loadById($id) {
226 return new MTrackProject($id);
229 static function loadByName($name) {
230 list($row) = MTrackDB::q('select projid from projects where shortname = ?',
232 if (isset($row[0])) {
233 return self::loadById($row[0]);
238 function __construct($id = null) {
240 list($row) = MTrackDB::q(
241 'select * from projects where projid = ?',
243 if (isset($row[0])) {
244 $this->projid = $row['projid'];
245 $this->ordinal = $row['ordinal'];
246 $this->name = $row['name'];
247 $this->shortname = $row['shortname'];
248 $this->notifyemail = $row['notifyemail'];
251 throw new Exception("unable to find project with id = $id");
255 function save(MTrackChangeset $CS) {
257 list($row) = MTrackDB::q(
258 'select * from projects where projid = ?',
259 $this->projid)->fetchAll();
262 'update projects set ordinal = ?, name = ?, shortname = ?,
263 notifyemail = ? where projid = ?',
264 $this->ordinal, $this->name, $this->shortname,
265 $this->notifyemail, $this->projid);
267 MTrackDB::q('insert into projects (ordinal, name,
268 shortname, notifyemail) values (?, ?, ?, ?)',
269 $this->ordinal, $this->name, $this->shortname,
271 $this->projid = MTrackDB::lastInsertId('projects', 'projid');
274 $CS->add("project:" . $this->projid . ":name", $old['name'], $this->name);
275 $CS->add("project:" . $this->projid . ":ordinal", $old['ordinal'], $this->ordinal);
276 $CS->add("project:" . $this->projid . ":shortname", $old['shortname'], $this->shortname);
277 $CS->add("project:" . $this->projid . ":notifyemail", $old['notifyemail'], $this->notifyemail);
280 function _adjust_ticket_link($M) {
281 $tktlimit = MTrackConfig::get('trac_import', "max_ticket:$this->shortname");
282 if ($M[1] <= $tktlimit) {
283 return "#$this->shortname$M[1]";
288 function adjust_links($reason, $use_ticket_prefix)
290 if (!$use_ticket_prefix) {
294 $tktlimit = MTrackConfig::get('trac_import', "max_ticket:$this->shortname");
295 if ($tktlimit !== null) {
296 $reason = preg_replace_callback('/#(\d+)/',
297 array($this, '_adjust_ticket_link'), $reason);
299 // don't do this if the number is outside the valid ranges
300 // may need to be clever about this during trac imports
301 // $reason = preg_replace('/#(\d+)/', "#$this->shortname\$1", $reason);
303 // FIXME: this and the above need to be more intelligent
304 $reason = preg_replace('/\[(\d+)\]/', "[$this->shortname\$1]", $reason);
309 /* The listener protocol is to return true if all is good,
310 * or to return either a string or an array of strings that
311 * detail why a change is not allowed to proceed */
312 interface IMTrackIssueListener {
313 function vetoMilestone(MTrackIssue $issue,
314 MTrackMilestone $ms, $assoc = true);
315 function vetoKeyword(MTrackIssue $issue,
316 MTrackKeyword $kw, $assoc = true);
317 function vetoComponent(MTrackIssue $issue,
318 MTrackComponent $comp, $assoc = true);
319 function vetoProject(MTrackIssue $issue,
320 MTrackProject $proj, $assoc = true);
321 function vetoComment(MTrackIssue $issue, $comment);
322 function vetoSave(MTrackIssue $issue, $oldFields);
324 function augmentFormFields(MTrackIssue $issue, &$fieldset);
325 function applyPOSTData(MTrackIssue $issue, $data);
326 function augmentSaveParams(MTrackIssue $issue, &$params);
327 function augmentIndexerFields(MTrackIssue $issue, &$idx);
330 class MTrackVetoException extends Exception {
333 function __construct($reasons) {
334 $this->reasons = $reasons;
335 parent::__construct(join("\n", $reasons));
341 public $nsident = null;
342 public $summary = null;
343 public $description = null;
344 public $created = null;
345 public $updated = null;
346 public $owner = null;
347 public $priority = null;
348 public $severity = null;
349 public $classification = null;
350 public $resolution = null;
351 public $status = null;
352 public $estimated = null;
353 public $spent = null;
354 public $changelog = null;
356 protected $components = null;
357 protected $origcomponents = null;
358 protected $milestones = null;
359 protected $origmilestones = null;
360 protected $comments_to_add = array();
361 protected $keywords = null;
362 protected $origkeywords = null;
363 protected $effort = array();
365 static $_listeners = array();
367 static function loadById($id) {
369 return new MTrackIssue($id);
370 } catch (Exception $e) {
375 static function loadByNSIdent($id) {
376 static $cache = array();
377 if (!isset($cache[$id])) {
378 $ids = MTrackDB::q('select tid from tickets where nsident = ?', $id)
379 ->fetchAll(PDO::FETCH_COLUMN, 0);
380 if (count($ids) == 1) {
381 $cache[$id] = $ids[0];
386 return new MTrackIssue($cache[$id]);
389 static function registerListener(IMTrackIssueListener $l)
391 self::$_listeners[] = $l;
394 function __construct($tid = null) {
396 $this->components = array();
397 $this->origcomponents = array();
398 $this->milestones = array();
399 $this->origmilestones = array();
400 $this->keywords = array();
401 $this->origkeywords = array();
402 $this->status = 'new';
404 foreach (array('classification', 'severity', 'priority') as $f) {
405 $this->$f = MTrackConfig::get('ticket', "default.$f");
408 $data = MTrackDB::q('select * from tickets where tid = ?',
410 if (isset($data[0])) {
415 if (!is_array($row)) {
416 throw new Exception("no such issue $tid");
418 foreach ($row as $k => $v) {
424 function applyPOSTData($data) {
425 foreach (self::$_listeners as $l) {
426 $l->applyPOSTData($this, $data);
430 function augmentFormFields(&$FIELDSET) {
431 foreach (self::$_listeners as $l) {
432 $l->augmentFormFields($this, $FIELDSET);
435 function augmentIndexerFields(&$idx) {
436 foreach (self::$_listeners as $l) {
437 $l->augmentIndexerFields($this, $idx);
440 function augmentSaveParams(&$params) {
441 foreach (self::$_listeners as $l) {
442 $l->augmentSaveParams($this, $params);
448 $args = func_get_args();
449 $method = array_shift($args);
452 foreach (self::$_listeners as $l) {
453 $v = call_user_func_array(array($l, $method), $args);
460 foreach ($veto as $r) {
469 throw new MTrackVetoException($reasons);
473 function save(MTrackChangeset $CS)
475 $db = MTrackDB::get();
478 if ($this->tid === null) {
479 $this->created = $CS->cid;
483 list($oldrow) = MTrackDB::q('select * from tickets where tid = ?',
484 $this->tid)->fetchAll();
487 $this->checkVeto('vetoSave', $this, $oldrow);
489 $this->updated = $CS->cid;
492 'summary' => $this->summary,
493 'description' => $this->description,
494 'created' => $this->created,
495 'updated' => $this->updated,
496 'owner' => $this->owner,
497 'changelog' => $this->changelog,
498 'priority' => $this->priority,
499 'severity' => $this->severity,
500 'classification' => $this->classification,
501 'resolution' => $this->resolution,
502 'status' => $this->status,
503 'estimated' => (float)$this->estimated,
504 'spent' => (float)$this->spent,
505 'nsident' => $this->nsident,
509 $this->augmentSaveParams($params);
511 if ($this->tid === null) {
512 $sql = 'insert into tickets ';
516 $new_tid = new OmniTI_Util_UUID;
517 $new_tid = $new_tid->toRFC4122String(false);
520 $values[] = "'$new_tid'";
522 foreach ($params as $key => $value) {
527 $sql .= "(" . join(', ', $keys) . ") values (" .
528 join(', ', $values) . ")";
530 $sql = 'update tickets set ';
532 foreach ($params as $key => $value) {
533 $values[] = "$key = :$key";
535 $sql .= join(', ', $values) . " where tid = :tid";
537 $params['tid'] = $this->tid;
540 $q = $db->prepare($sql);
541 $q->execute($params);
543 if ($this->tid === null) {
544 $this->tid = $new_tid;
550 foreach ($params as $key => $value) {
551 if ($key == 'created' || $key == 'updated' || $key == 'tid') {
554 if ($key == 'changelog' || $key == 'description' || $key == 'summary') {
555 if (!isset($oldrow[$key]) || $oldrow[$key] != $value) {
559 if (!isset($oldrow[$key])) {
560 $oldrow[$key] = null;
562 $CS->add("ticket:$this->tid:$key", $oldrow[$key], $value);
565 $this->compute_diff($CS, 'components', 'ticket_components', 'compid',
566 $this->components, $this->origcomponents);
567 $this->compute_diff($CS, 'keywords', 'ticket_keywords', 'kid',
568 $this->keywords, $this->origkeywords);
569 $this->compute_diff($CS, 'milestones', 'ticket_milestones', 'mid',
570 $this->milestones, $this->origmilestones);
572 foreach ($this->comments_to_add as $text) {
573 $CS->add("ticket:$this->tid:@comment", null, $text);
576 foreach ($this->effort as $effort) {
577 MTrackDB::q('insert into effort (tid, cid, expended, remaining)
578 values (?, ?, ?, ?)',
579 $this->tid, $CS->cid, $effort[0], $effort[1]);
581 $this->effort = array();
584 static function index_issue($object)
586 list($ignore, $ident) = explode(':', $object, 2);
587 $i = MTrackIssue::loadById($ident);
589 echo "Ticket #$i->nsident\n";
591 $CS = MTrackChangeset::get($i->updated);
592 $CSC = MTrackChangeset::get($i->created);
594 $kw = join(' ', array_values($i->getKeywords()));
596 'summary' => $i->summary,
597 'description' => $i->description,
598 'changelog' => $i->changelog,
600 'stored:date' => $CS->when,
602 'creator' => $CSC->who,
603 'stored:created' => $CSC->when,
606 $i->augmentIndexerFields($idx);
607 MTrackSearchDB::add("ticket:$i->tid", $idx, true);
609 foreach (MTrackDB::q('select value, changedate, who from
610 change_audit left join changes using (cid) where fieldname = ?',
611 "ticket:$ident:@comment") as $row) {
612 list($text, $when, $who) = $row;
615 $elapsed = time() - $start;
617 echo " - comment $who $when took $elapsed to hash\n";
620 if (strlen($text) > 8192) {
621 // A huge paste into a ticket
622 $text = substr($text, 0, 8192);
624 MTrackSearchDB::add("ticket:$ident:comment:$id", array(
625 'description' => $text,
626 'stored:date' => $when,
630 $elapsed = time() - $start;
632 echo " - comment $who $when took $elapsed to index\n";
637 private function compute_diff(MTrackChangeset $CS, $label,
638 $tablename, $keyname, $current, $orig) {
639 if (!is_array($current)) {
642 if (!is_array($orig)) {
645 $added = array_keys(array_diff_key($current, $orig));
646 $removed = array_keys(array_diff_key($orig, $current));
648 $db = MTrackDB::get();
650 "insert into $tablename (tid, $keyname) values (?, ?)");
652 "delete from $tablename where tid = ? AND $keyname = ?");
653 foreach ($added as $key) {
654 $ADD->execute(array($this->tid, $key));
656 foreach ($removed as $key) {
657 $DEL->execute(array($this->tid, $key));
659 if (count($added) + count($removed)) {
660 $old = join(',', array_keys($orig));
661 $new = join(',', array_keys($current));
663 "ticket:$this->tid:@$label", $old, $new);
666 function getComponents()
668 if ($this->components === null) {
669 $comps = MTrackDB::q('select tc.compid, name from ticket_components tc left join components using (compid) where tid = ?', $this->tid)->fetchAll();
670 $this->origcomponents = array();
671 foreach ($comps as $row) {
672 $this->origcomponents[$row[0]] = $row[1];
674 $this->components = $this->origcomponents;
676 return $this->components;
679 private function resolveComponent($comp)
681 if ($comp instanceof MTrackComponent) {
684 if (ctype_digit($comp)) {
685 return MTrackComponent::loadById($comp);
687 return MTrackComponent::loadByName($comp);
690 function assocComponent($comp)
692 $comp = $this->resolveComponent($comp);
693 $this->getComponents();
694 $this->checkVeto('vetoComponent', $this, $comp, true);
695 $this->components[$comp->compid] = $comp->name;
698 function dissocComponent($comp)
700 $comp = $this->resolveComponent($comp);
701 $this->getComponents();
702 $this->checkVeto('vetoComponent', $this, $comp, false);
703 unset($this->components[$comp->compid]);
706 function getMilestones()
708 if ($this->milestones === null) {
709 $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();
710 $this->origmilestones = array();
711 foreach ($comps as $row) {
712 $this->origmilestones[$row[0]] = $row[1];
714 $this->milestones = $this->origmilestones;
716 return $this->milestones;
719 private function resolveMilestone($ms)
721 if ($ms instanceof MTrackMilestone) {
724 if (ctype_digit($ms)) {
725 return MTrackMilestone::loadById($ms);
727 return MTrackMilestone::loadByName($ms);
730 function assocMilestone($M)
732 $ms = $this->resolveMilestone($M);
734 throw new Exception("unable to resolve milestone $M");
736 $this->getMilestones();
737 $this->checkVeto('vetoMilestone', $this, $ms, true);
738 $this->milestones[$ms->mid] = $ms->name;
741 function dissocMilestone($M)
743 $ms = $this->resolveMilestone($M);
745 throw new Exception("unable to resolve milestone $M");
747 $this->getMilestones();
748 $this->checkVeto('vetoMilestone', $this, $ms, false);
749 unset($this->milestones[$ms->mid]);
752 function addComment($comment)
754 $comment = trim($comment);
755 if (strlen($comment)) {
756 $this->checkVeto('vetoComment', $this, $comment);
757 $this->comments_to_add[] = $comment;
761 private function resolveKeyword($kw)
763 if ($kw instanceof MTrackKeyword) {
766 $k = MTrackKeyword::loadByWord($kw);
768 if (ctype_digit($kw)) {
769 return MTrackKeyword::loadById($kw);
771 throw new Exception("unknown keyword $kw");
776 function assocKeyword($kw)
778 $kw = $this->resolveKeyword($kw);
779 $this->getKeywords();
780 $this->checkVeto('vetoKeyword', $this, $kw, true);
781 $this->keywords[$kw->kid] = $kw->keyword;
784 function dissocKeyword($kw)
786 $kw = $this->resolveKeyword($kw);
787 $this->getKeywords();
788 $this->checkVeto('vetoKeyword', $this, $kw, false);
789 unset($this->keywords[$kw->kid]);
792 function getKeywords()
794 if ($this->keywords === null) {
795 $comps = MTrackDB::q('select tc.kid, keyword from ticket_keywords tc left join keywords using (kid) where tid = ?', $this->tid)->fetchAll();
796 $this->origkeywords = array();
797 foreach ($comps as $row) {
798 $this->origkeywords[$row[0]] = $row[1];
800 $this->keywords = $this->origkeywords;
802 return $this->keywords;
805 function addEffort($amount, $revised = null)
808 if ($revised !== null) {
809 $diff = $revised - $this->estimated;
810 $this->estimated = $revised;
812 $this->effort[] = array($amount, $diff);
813 $this->spent += $amount;
818 $this->status = 'closed';
819 $this->addEffort(0, 0);
824 switch ($this->status) {
834 $this->status = 'reopened';
835 $this->resolution = null;
840 MTrackSearchDB::register_indexer('ticket', array('MTrackIssue', 'index_issue'));
841 MTrackACL::registerAncestry('enum', 'Enumerations');
842 MTrackACL::registerAncestry("component", 'Components');
843 MTrackACL::registerAncestry("project", 'Projects');
844 MTrackACL::registerAncestry("ticket", "Tickets");
845 MTrackWatch::registerEventTypes('ticket', array(
846 'ticket' => 'Tickets'