import
[web.mtrack] / inc / web.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 /* Simplistic pathinfo parsing - could optionally have additional features such
5   as validation added */
6 function mtrack_parse_pathinfo($vars) {
7   $pi = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
8   $data = explode('/', $pi);
9   $i = 0;
10   $return_vars = array();
11   array_shift($data);
12   foreach($vars as $name => $value) {
13     if (isset($data[$i])) {
14       $return_vars[$name] = $data[$i];
15       $i++;
16     } else {
17       $return_vars[$name] = $value;
18     }
19   }
20   return $return_vars;
21 }
22
23 /* Pathinfo retrieval minus starting slash */
24 function mtrack_get_pathinfo($no_strip = false) {
25   $pi = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : NULL;
26   if ($pi !== NULL && strlen($pi) && $no_strip == false) {
27     $pi = substr($pi, 1);
28   }
29   return $pi;
30 }
31
32 function mtrack_calc_root()
33 {
34   /* ABSWEB: the absolute URL to the base of the web app */
35   global $ABSWEB;
36
37   /* if they have one, use the weburl config value for this */
38   $ABSWEB = MTrackConfig::get('core', 'weburl');
39   if (strlen($ABSWEB)) {
40     return;
41   }
42
43   /* otherwise, determine the root of the app.
44    * This is complicated because the DOCUMENT_ROOT may refer to an area that
45    * is completely unrelated to the actual root of the web application, for
46    * instance, in the case that the user has a public_html dir where they
47    * are running mtrack */
48
49   /* determine the root of the app */
50   $sdir = dirname($_SERVER['SCRIPT_FILENAME']);
51   $idir = dirname(dirname(__FILE__)) . '/web';
52   $diff = substr($sdir, strlen($idir)+1);
53   $rel = preg_replace('@[^/]+@', '..', $diff);
54   if (strlen($rel)) {
55     $rel .= '/';
56   }
57   /* $rel is now the relative path to the root of the web app, from the current
58    * page */
59
60   if (isset($_SERVER['HTTP_HOST'])) {
61     $ABSWEB = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ?
62               'https' : 'http') . '://' .  $_SERVER['HTTP_HOST'];
63   } else {
64     $ABSWEB = 'http://localhost';
65   }
66
67   $bits = explode('/', $rel);
68   $base = $_SERVER['SCRIPT_NAME'];
69   foreach ($bits as $b) {
70     $base = dirname($base);
71   }
72   if ($base == '/') {
73     $ABSWEB .= '/';
74   } else {
75     $ABSWEB .= $base . '/';
76   }
77 }
78 mtrack_calc_root();
79
80 function mtrack_head($title, $navbar = true)
81 {
82   global $ABSWEB;
83   static $mtrack_did_head;
84
85   $whoami = mtrack_username(MTrackAuth::whoami(),
86     array(
87       'no_image' => true
88     )
89   );
90
91   if ($mtrack_did_head) {
92     return;
93   }
94   $mtrack_did_head = true;
95
96   $projectname = htmlentities(MTrackConfig::get('core', 'projectname'),
97     ENT_QUOTES, 'utf-8');
98   $logo = MTrackConfig::get('core', 'projectlogo');
99   if (strlen($logo)) {
100     $projectname = "<img alt='$projectname' src='$logo'>";
101   }
102   $fav = MTrackConfig::get('core', 'favicon');
103   if (strlen($fav)) {
104     $fav = <<<HTML
105 <link rel="icon" href="$fav" type="image/x-icon" />
106 <link rel="shortcut icon" href="$fav" type="image/x-icon" />
107 HTML;
108   } else {
109     $fav = '';
110   }
111
112   $title = htmlentities($title, ENT_QUOTES, 'utf-8');
113
114   $userinfo = "Logged in as $whoami";
115   MTrackNavigation::augmentUserInfo($userinfo);
116
117   echo <<<HTML
118 <!DOCTYPE html>
119 <html>
120 <head>
121 <meta http-equiv="Content-Type" value="text/html; charset=utf-8">
122 <meta http-equiv="X-UA-Compatible" content="IE=8">
123 <title>$title</title>
124 $fav
125 <link rel="stylesheet" href="${ABSWEB}css.php" type="text/css" />
126 <script language="javascript" type="text/javascript" src="${ABSWEB}js.php"></script>
127 </head>
128 <body>
129 HTML;
130
131   if ($navbar) {
132     echo <<<HTML
133 <div id="banner-back">
134   <form id="mainsearch" action="${ABSWEB}search.php">
135     $userinfo
136     <input type="text" class="search" title="Type and press enter to Search"
137         name="q" accesskey="f">
138   </form>
139   <div id="banner">
140     $projectname
141   </div>
142 HTML;
143
144     echo <<<HTML
145 <div id="header">
146 HTML;
147
148   $nav = array();
149   if (MTrackAuth::whoami() !== 'anonymous') {
150     $nav['/'] = 'Today';
151   }
152   $navcandidates = array(
153     "/browse.php" => array("Browse", 'read', 'Browser'),
154     "/wiki.php" => array("Wiki", 'read', 'Wiki'),
155     "/timeline.php" => array("Timeline", 'read', 'Timeline'),
156     "/roadmap.php" => array("Roadmap", 'read', 'Roadmap'),
157     "/reports.php" => array("Reports", 'read', 'Reports'),
158     "/ticket.php/new" => array("New Ticket", 'create', 'Tickets'),
159     "/snippet.php" => array("Snippets", 'read', 'Snippets'),
160     "/admin/" => array("Administration", 'modify', 'Enumerations', 'Components', 'Projects', 'Browser'),
161   );
162   foreach ($navcandidates as $url => $data) {
163     $label = array_shift($data);
164     $right = array_shift($data);
165     $ok = false;
166     foreach ($data as $object) {
167       if (MTrackACL::hasAllRights($object, $right)) {
168         $ok = true;
169         break;
170       }
171     }
172     if ($ok) {
173       $nav[$url] = $label;
174     }
175   }
176
177   echo mtrack_nav('mainnav', $nav);
178   echo <<<HTML
179   </div>
180 HTML;
181   }
182   if (MTrackConfig::get('core', 'admin_party') == 1 &&
183       MTrackAuth::whoami() == 'adminparty' &&
184       ($_SERVER['REMOTE_ADDR'] == '127.0.0.1' ||
185           $_SERVER['REMOTE_ADDR'] == '::1')) {
186     echo <<<HTML
187 <div class='ui-state-error ui-corner-all'>
188     <span class='ui-icon ui-icon-alert'></span>
189   <b>Welcome to the admin party!</b> Authentication is not yet configured;
190   while it is in this state, any user connecting from the localhost
191   address is treated as having admin rights (that includes you, and this
192   is why you are seeing this message). All other users are treated
193   as anonymous users.<br>
194   <b><a href="{$ABSWEB}admin/auth.php">Click here to Configure Authentication</a></b>
195 </div>
196 HTML;
197   } elseif (!MTrackAuth::isAuthConfigured() &&
198       MTrackConfig::get('core', 'admin_party') == 1)
199   {
200     $localaddr = preg_replace('@^(https?://)([^/]+)/(.*)$@',
201       "\\1localhost/\\3", $ABSWEB);
202
203     echo <<<HTML
204 <div class='ui-state-highlight ui-corner-all'>
205     <span class='ui-icon ui-icon-info'></span>
206   <b>Authentication is not yet configured</b>.  If you are the admin,
207   you should use the <b><a href="$localaddr">localhost address</a></b>
208   to reach the system and configure it.
209 </div>
210 HTML;
211   } elseif (!MTrackAuth::isAuthConfigured()) {
212     echo <<<HTML
213 <div class='ui-state-highlight ui-corner-all'>
214     <span class='ui-icon ui-icon-info'></span>
215   <b>Authentication is not yet configured</b>.  If you are the admin,
216   you will need to edit the config.ini file to configure authentication.
217 </div>
218 HTML;
219   }
220
221   if (ini_get('magic_quotes_gpc') === true ||
222       !strcasecmp(ini_get('magic_quotes_gpc'), 'on')) {
223     echo <<<HTML
224 <div class='ui-state-error ui-corner-all'>
225     <span class='ui-icon ui-icon-alert'></span>
226   <b>magic_quotes_gpc</b> is enabled.  This causes mtrack not to work.
227   Please disable this setting in your server configuration.
228 </div>
229 HTML;
230
231   }
232
233   echo <<<HTML
234 </div>
235 <div id="content">
236 HTML;
237 }
238
239 function mtrack_foot($visible_markup = true)
240 {
241   echo <<<HTML
242 </div>
243 HTML;
244   if ($visible_markup) {
245     echo <<<HTML
246 <div id="footer">
247 <div class="navfoot">
248   Powered by <a href="http://bitbucket.org/wez/mtrack/">mtrack</a>
249 </div>
250 </div>
251 </body>
252 <script>
253 \$(document).ready(function () {
254   window.mtrack_footer_position();
255 });
256 </script>
257 </html>
258 HTML;
259     if (MTrackConfig::get('core', 'debug.footer')) {
260       global $FORKS;
261
262       echo "<!-- " . MTrackDB::$queries . " queries\n";
263       var_export(MTrackDB::$query_strings);
264       echo "\n\nforks\n\n";
265       var_export($FORKS);
266       echo "-->";
267     }
268   }
269 }
270
271 interface IMTrackExtensionPage {
272   /** called to dispatch a page render */
273   function dispatchRequest();
274 }
275
276 class MTrackExtensionPage {
277   static $locations = array();
278   static function registerLocation($location, IMTrackExtensionPage $page) {
279     self::$locations[$location] = $page;
280   }
281   static function locationToURL($location) {
282     global $ABSWEB;
283     return $ABSWEB . 'ext.php/' . $location;
284   }
285   static function bindToPage($location) {
286     while (strlen($location)) {
287       if (isset(self::$locations[$location])) {
288         return self::$locations[$location];
289       }
290       if (strpos($location, '/') === false) {
291         return null;
292       }
293       $location = dirname($location);
294     }
295   }
296 }
297
298 interface IMTrackNavigationHelper {
299   /** called by mtrack_nav
300    * You may remove items from or add items to the items array by
301    * changing the $items array.
302    * Should you want to suppress the Wiki from navigation, you may
303    * do so like this:
304    * if ($id == 'mainnav') {
305    *   unset($items['/wiki.php']);
306    * }
307    * If you want to add an item, the key is the URL and the value
308    * is the label.  The label is raw HTML.
309    */
310   function augmentNavigation($id, &$items);
311
312   /** called by mtrack_head
313    * You may augment or override the "Logged in as user" text by
314    * changing the $content variable */
315   function augmentUserInfo(&$content);
316 }
317
318 class MTrackNavigation {
319   static $helpers = array();
320
321   static function registerHelper(IMTrackNavigationHelper $helper)
322   {
323     self::$helpers[] = $helper;
324   }
325
326   static function augmentNavigation($id, &$items)
327   {
328     foreach (self::$helpers as $helper) {
329       $helper->augmentNavigation($id, $items);
330     }
331   }
332
333   static function augmentUserInfo(&$content)
334   {
335     foreach (self::$helpers as $helper) {
336       $helper->augmentUserInfo($content);
337     }
338   }
339 }
340
341 function mtrack_nav($id, $nav) {
342   global $ABSWEB;
343
344   // Allow config file to manipulate the navigation bits
345   $cnav = MTrackConfig::getSection('nav:' . $id);
346   if (is_array($cnav)) {
347     foreach ($cnav as $loc => $label) {
348       if (!strlen($label)) {
349         unset($nav[$loc]);
350       } else {
351         $nav[$loc] = $label;
352       }
353     }
354   }
355
356   MTrackNavigation::augmentNavigation($id, $nav);
357
358   $elements = array();
359
360   $web = realpath(dirname(__FILE__) . '/../web');
361   $where = substr($_SERVER['SCRIPT_FILENAME'], strlen($web));
362   if (isset($_SERVER['PATH_INFO'])) {
363     $where .= $_SERVER['PATH_INFO'];
364   }
365   $active = null;
366   $tries = 0;
367   do {
368     foreach ($nav as $loc => $label) {
369       $cloc = $loc;
370       if (!strncmp($cloc, $ABSWEB, strlen($ABSWEB))) {
371         $cloc = substr($cloc, strlen($ABSWEB)-1);
372       }
373       if ($where == $cloc || $where == rtrim($cloc, '/')) {
374         $active = $loc;
375         break;
376       }
377     }
378     $where = dirname($where);
379   } while ($active === null && $tries++ < 100);
380
381   foreach ($nav as $loc => $label) {
382     unset($nav[$loc]);
383     $class = array();
384     if (!count($elements)) {
385       $class[] = "first";
386     }
387     if (count($nav) == 0) {
388       $class[] = "last";
389     }
390     if ($active == $loc) {
391       $class[] = 'active';
392     }
393     if (count($class)) {
394       $class = " class=\"" . implode(' ', $class) . "\"";
395     } else {
396       $class = '';
397     }
398     if ($loc[0] == '/') {
399       $url = substr($loc, 1); // trim off leading /
400     } else {
401       $url = $loc;
402     }
403     if (!preg_match('/^[a-z-]+:/', $url)) {
404       $url = $ABSWEB . $url;
405     }
406     $elements[] = "<li$class><a href=\"$url\">$label</a></li>";
407   }
408   return "<div id='$id' class='nav'><ul>" .
409     implode('', $elements) . "</ul></div>";
410 }
411
412 function mtrack_date($tstring, $show_full = false)
413 {
414   /* database time is always relative to UTC */
415   $d = date_create($tstring, new DateTimeZone('UTC'));
416   if (!is_object($d)) {
417     throw new Exception("could not represent $tstring as a datetime object");
418   }
419   $iso8601 = $d->format(DateTime::W3C);
420   /* but we want to render relative to user prefs */
421   date_timezone_set($d, new DateTimeZone(date_default_timezone_get()));
422   $full = $d->format('D, M d Y H:i');
423
424   if (!$show_full) {
425     return "<abbr title=\"$iso8601\" class='timeinterval'>$full</abbr>";
426   }
427
428   return "<abbr title='$iso8601' class='timeinterval'>$full</abbr> <span class='fulldate'>$full</span>";
429 }
430
431 function mtrack_rmdir($dir)
432 {
433   foreach (scandir($dir) as $ent) {
434     if ($ent == '.' || $ent == '..') {
435       continue;
436     }
437     $full = $dir . DIRECTORY_SEPARATOR . $ent;
438     if (is_dir($full)) {
439       mtrack_rmdir($full);
440     } else {
441       unlink($full);
442     }
443   }
444   rmdir($dir);
445 }
446
447 function mtrack_make_temp_dir($do_make = true)
448 {
449   $tempdir = sys_get_temp_dir();
450   $base = $tempdir . DIRECTORY_SEPARATOR . "mtrack." . uniqid();
451   for ($i = 0; $i < 1024; $i++) {
452     $candidate = $base . sprintf("%04x", $i);
453     if ($do_make) {
454       if (mkdir($candidate)) {
455         return $candidate;
456       }
457     } else {
458       /* racy */
459       if (!file_exists($candidate) && !is_dir($candidate)) {
460         return $candidate;
461       }
462     }
463   }
464   throw new Exception("unable to make temp dir based on path $candidate");
465 }
466
467 function mtrack_diff_strings($before, $now)
468 {
469   $tempdir = sys_get_temp_dir();
470   $afile = tempnam($tempdir, "mtrack");
471   $bfile = tempnam($tempdir, "mtrack");
472   file_put_contents($afile, $before);
473   file_put_contents($bfile, $now);
474   $diff = MTrackConfig::get('tools', 'diff');
475   if (PHP_OS == 'SunOS') {
476      // TODO: make an option to allow use of gnu diff on solaris
477     $diff = shell_exec("$diff -u $afile $bfile");
478     $diff = str_replace($afile, 'before', $diff);
479     $diff = str_replace($bfile, 'now', $diff);
480   } else {
481     $diff = shell_exec("$diff --label before --label now -u $afile $bfile");
482   }
483   unlink($afile);
484   unlink($bfile);
485   $diff = htmlentities($diff, ENT_COMPAT, 'utf-8');
486   return $diff;
487 }
488
489 function mtrack_last_chance_saloon($e)
490 {
491   if ($e instanceof MTrackAuthorizationException) {
492     if (MTrackAuth::whoami() == 'anonymous') {
493       MTrackAuth::forceAuthenticate();
494     }
495     mtrack_head('Insufficient Privilege');
496     echo '<h1>Insufficient Privilege</h1>';
497     $rights = is_array($e->rights) ? join(', ', $e->rights) : $e->rights;
498     echo "You do not have the required set of rights ($rights) to access this page<br>";
499     mtrack_foot();
500     exit;
501   }
502
503   $msg = $e->getMessage();
504
505   try {
506     mtrack_head('Whoops: ' . $msg);
507   } catch (Exception $doublefault) {
508   }
509
510   echo "<h1>An error occurred!</h1>";
511
512   echo htmlentities($msg, ENT_QUOTES, 'utf-8');
513
514   echo "<br>";
515
516   echo nl2br(htmlentities($e->getTraceAsString(), ENT_QUOTES, 'utf-8'));
517
518   try {
519     mtrack_foot();
520   } catch (Exception $doublefault) {
521   }
522 }
523
524 function mtrack_canon_username($username)
525 {
526   static $canon_map = null;
527
528   if ($canon_map === null) {
529     $canon_map = array();
530     foreach (MTrackDB::q('select alias, userid from useraliases union select email, userid from userinfo where email <> \'\'')->fetchAll()
531         as $row) {
532       $canon_map[$row[0]] = $row[1];
533     }
534   }
535
536   $runaway = 25;
537   do {
538     if (isset($canon_map[$username])) {
539       if ($username == $canon_map[$username]) {
540         break;
541       }
542       $username = $canon_map[$username];
543     } elseif (preg_match('/<([a-z0-9_.+=-]+@[a-z0-9.-]+)>/', $username, $M)) {
544       // look at just the email address
545       $username = $M[1];
546       if (!isset($canon_map[$username])) {
547         break;
548       }
549     } else {
550       break;
551     }
552   } while ($runaway-- > 0);
553
554   return $username;
555 }
556
557 function mtrack_username($username, $options = array())
558 {
559   $username = mtrack_canon_username($username);
560   $userdata = MTrackAuth::getUserData($username);
561
562   if (isset($userdata['fullname']) && strlen($userdata['fullname'])) {
563     $title = " title='" .
564         htmlentities($userdata['fullname'], ENT_QUOTES, 'utf-8') . "' ";
565   } else {
566     $title = '';
567   }
568
569   global $ABSWEB;
570
571   if (!isset($options['size'])) {
572     $options['size'] = 24;
573   }
574   if (isset($options['class'])) {
575     $extraclass = " $options[class]";
576   } else {
577     $extraclass = '';
578   }
579
580   if (!ctype_alnum($username)) {
581     $target = "{$ABSWEB}user.php?user=" . urlencode($username);
582     if (isset($options['edit'])) {
583       $target .= '&edit=1';
584     }
585   } else {
586     $target = "{$ABSWEB}user.php/$username";
587     if (isset($options['edit'])) {
588       $target .= '?edit=1';
589     }
590   }
591   $open_a = "<a $title href='$target' class='userlink$extraclass'>";
592
593   $ret = '';
594   if ((!isset($options['no_image']) || !$options['no_image'])) {
595     $ret .= $open_a .
596             mtrack_avatar($username, $options['size']) .
597             '</a> ';
598   }
599   if (!isset($options['no_name']) || !$options['no_name']) {
600     $dispuser = $username;
601
602     if (strlen($dispuser) > 12) {
603       if (preg_match("/^([^+]*)(\+.*)?@(.*)$/", $dispuser, $M)) {
604         /* looks like an email address, try to shorten it in a reasonable way */
605         $local = $M[1];
606         $extra = $M[2];
607         $domain = $M[3];
608
609         if (strlen($extra)) {
610           $local .= '...';
611         }
612
613         $dispuser = "$local@$domain";
614       }
615     }
616     $ret .= "$open_a$dispuser</a>";
617   }
618   return $ret;
619 }
620
621 function mtrack_avatar($username, $size = 24)
622 {
623   global $ABSWEB;
624
625   $id = urlencode($username);
626
627   return "<img class='gravatar' width='$size' height='$size' src='{$ABSWEB}avatar.php?u=$id&amp;s=$size'>";
628 }
629
630 function mtrack_gravatar($email, $size = 24)
631 {
632   // d=identicon
633   // d=monsterid
634   // d=wavatar
635   return "<img class='gravatar' width='$size' height='$size' src='http://www.gravatar.com/avatar/" .  md5(strtolower($email)) . "?s=$size&amp;d=wavatar'>";
636 }
637
638 function mtrack_defrepo()
639 {
640   static $defrepo = null;
641   if ($defrepo === null) {
642     $defrepo = MTrackConfig::get('core', 'default.repo');
643     if ($defrepo === null) {
644       $defrepo = '';
645       foreach (MTrackDB::q(
646           'select parent, shortname from repos order by shortname')
647           ->fetchAll() as $row) {
648         $defrepo = MTrackSCM::makeDisplayName($row);
649         break;
650       }
651     } else if (strpos($defrepo, '/') === false) {
652       $defrepo = 'default/' . $defrepo;
653     }
654   }
655   return $defrepo;
656 }
657
658 function mtrack_changeset_url($cs, $repo = null)
659 {
660   global $ABSWEB;
661   if ($repo instanceof MTrackRepo) {
662     $p = $repo->getBrowseRootName() . '/';
663   } elseif ($repo !== null) {
664     if (strpos($repo, '/') === false) {
665       $repo = "default/$repo";
666     }
667     $p = $repo . '/';
668   } else {
669     static $repos = null;
670     if ($repos === null) {
671       $repos = array();
672       foreach (MTrackDB::q('select r.shortname as repo, p.shortname as proj from repos r left join project_repo_link l using (repoid) left join projects p using (projid) where parent is null or length(parent) = 0')->fetchAll(PDO::FETCH_ASSOC) as $row) {
673         $r = $row['repo'];
674         if ($row['proj']) {
675           $repos[$row['proj']] = $r;
676         }
677         $repos[$row['repo']] = $r;
678       }
679     }
680     $p = null;
681     foreach ($repos as $a => $b) {
682       if (!strncasecmp($cs, $a, strlen($a))) {
683         $p = 'default/' . $b;
684         $cs = substr($cs, strlen($a));
685         break;
686       }
687     }
688     if ($p === null) {
689       $p = mtrack_defrepo();
690     }
691     $p .= '/';
692   }
693   return $ABSWEB . "changeset.php/$p$cs";
694 }
695
696 function mtrack_changeset($cs, $repo = null)
697 {
698   $display = $cs;
699   if (strlen($display) > 12) {
700     $display = substr($display, 0, 12);
701   }
702   $url = mtrack_changeset_url($cs, $repo);
703   return "<a class='changesetlink' href='$url'>[$display]</a>";
704 }
705
706 function mtrack_branch($branch, $repo = null)
707 {
708   return "<span class='branchname'>$branch</span>";
709 }
710
711 function mtrack_wiki($pagename, $extras = array())
712 {
713   global $ABSWEB;
714   if ($pagename instanceof MTrackWikiItem) {
715     $wiki = $pagename;
716   } else if (is_string($pagename)) {
717     $wiki = null;//MTrackWikiItem::loadByPageName($pagename);
718   } else {
719     // FIXME: hinted data from reports
720     throw new Exception("FIXME: wiki");
721   }
722   if ($wiki) {
723     $pagename = $wiki->pagename;
724   }
725   $html = "<a class='wikilink'";
726   if (isset($extras['#'])) {
727     $anchor = '#' . $extras['#'];
728   } else {
729     $anchor = '';
730   }
731   $html .= " href=\"{$ABSWEB}wiki.php/$pagename$anchor\">";
732   if (isset($extras['display'])) {
733     $html .= htmlentities($extras['display'], ENT_QUOTES, 'utf-8');
734   } else {
735     $html .= htmlentities($pagename, ENT_QUOTES, 'utf-8');
736   }
737   $html .= "</a>";
738   return $html;
739 }
740
741 function mtrack_ticket($no, $extras = array())
742 {
743   global $ABSWEB;
744
745   if ($no instanceof MTrackIssue) {
746     $tkt = $no;
747   } else if (is_string($no) || is_int($no)) {
748     static $cache = array();
749
750     if ($no[0] == '#') {
751       $no = substr($no, 1);
752     }
753
754     if (!isset($cache[$no])) {
755       $tkt = MTrackIssue::loadByNSIdent($no);
756       if (!$tkt) {
757         $tkt = MTrackIssue::loadById($no);
758       }
759       $cache[$no] = $tkt;
760     } else {
761       $tkt = $cache[$no];
762     }
763   } else {
764     // FIXME: hinted data from reports
765     $tkt = new stdClass;
766     $tkt->tid = $no['ticket'];
767     $tkt->summary = $no['summary'];
768     if (isset($no['state'])) {
769       $tkt->status = $no['state'];
770     } elseif (isset($no['status'])) {
771       $tkt->status = $no['status'];
772     } elseif (isset($no['__status__'])) {
773       $tkt->status = $no['__status__'];
774     } else {
775       $tkt->status = '';
776     }
777   }
778   if ($tkt == NULL) {
779     $tkt = new stdClass;
780     $tkt->tid = $no;
781     $tkt->summary = 'No such ticket';
782     $tkt->status = 'No such ticket';
783   }
784   $html = "<a class='ticketlink";
785   if ($tkt->status == 'closed') {
786     $html .= ' completed';
787   }
788   if (!empty($tkt->nsident)) {
789     $ident = $tkt->nsident;
790   } else {
791     $ident = $tkt->tid;
792   }
793   if (isset($extras['#'])) {
794     $anchor = '#' . $extras['#'];
795   } else {
796     $anchor = '';
797   }
798   $html .= "' href=\"{$ABSWEB}ticket.php/$ident$anchor\">";
799   if (isset($extras['display'])) {
800     $html .= htmlentities($extras['display'], ENT_QUOTES, 'utf-8');
801   } else {
802     $html .= '#' . htmlentities($ident, ENT_QUOTES, 'utf-8');
803   }
804   $html .= "</a>";
805   return $html;
806 }
807
808 function mtrack_tag($tag, $repo = null)
809 {
810   return "<span class='tagname'>$tag</span>";
811 }
812
813 function mtrack_keyword($keyword)
814 {
815   global $ABSWEB;
816   $kw = urlencode($keyword);
817   return "<a class='keyword' href='{$ABSWEB}search.php?q=keyword%3A$kw'>$keyword</span>";
818 }
819
820 function mtrack_multi_select_box($name, $title, $items, $values = null)
821 {
822   $title = htmlentities($title, ENT_QUOTES, 'utf-8');
823   $html = "<select id='$name' name='{$name}[]' multiple='multiple' title='$title'>";
824   foreach ($items as $k => $v) {
825     $html .= "<option value='" .
826       htmlspecialchars($k, ENT_QUOTES, 'utf-8') .
827       "'";
828     if (isset($values[$k])) {
829       $html .= ' selected';
830     }
831     $html .= ">" . htmlentities($v, ENT_QUOTES, 'utf-8') . "</option>\n";
832   }
833   return $html . "</select>";
834 }
835
836 function mtrack_select_box($name, $items, $value = null, $keyed = true)
837 {
838   $html = "<select id='$name' name='$name'>";
839   foreach ($items as $k => $v) {
840     $html .= "<option value='" .
841       htmlspecialchars($k, ENT_QUOTES, 'utf-8') .
842       "'";
843     if (($keyed && $value == $k) || (!$keyed && $value == $v)) {
844       $html .= ' selected';
845     }
846     $html .= ">" . htmlentities($v, ENT_QUOTES, 'utf-8') . "</option>\n";
847   }
848   return $html . "</select>";
849 }
850
851 function mtrack_radio($name, $value, $curval)
852 {
853   $checked = $curval == $value ? " checked='checked'": '';
854   return "<input type='radio' id='$value' name='$name' value='$value'$checked>";
855 }
856
857 function mtrack_diff($diffstr)
858 {
859   $nlines = 0;
860
861   if (is_resource($diffstr)) {
862     $lines = array();
863     while (($line = fgets($diffstr)) !== false) {
864       $lines[] = rtrim($line, "\r\n");
865     }
866     $diffstr = $lines;
867   }
868
869   if (is_string($diffstr)) {
870     $abase = md5($diffstr);
871     $diffstr = preg_split("/\r?\n/", $diffstr);
872   } else {
873     $abase = md5(join("\n", $diffstr));
874   }
875
876   /* we could use toggle() below, but it is much faster to determine
877    * if we are hiding or showing based on a single variable than evaluating
878    * that for each possible cell */
879   $html = <<<HTML
880 <button class='togglediffcopy' type='button'>Toggle Diff Line Numbers</button>
881 HTML;
882   $html .= "<table class='code diff'>";
883   //$html = "<pre class='code diff'>";
884
885   while (true) {
886     if (!count($diffstr)) {
887       break;
888     }
889     $line = array_shift($diffstr);
890     $nlines++;
891     if (!strncmp($line, '@@ ', 3)) {
892       /* done with preamble */
893       break;
894     }
895     $line = htmlspecialchars($line, ENT_QUOTES, 'utf-8');
896     $line = "<tr class='meta'><td class='lineno'></td><td class='lineno'></td><td class='lineno'></td><td width='100%'>$line</tr>";
897     $html .= $line . "\n";
898   }
899
900   $lines = array(0, 0);
901   $first = false;
902   while (true) {
903     $class = 'unmod';
904
905     if (preg_match("/^@@\s+-(\pN+)(?:,\pN+)?\s+\+(\pN+)(?:,\pN+)?\s*@@/",
906         $line, $M)) {
907       $lines[0] = (int)$M[1] - 1;
908       $lines[1] = (int)$M[2] - 1;
909       $class = 'meta';
910       $first = true;
911     } elseif (strlen($line)) {
912       if ($line[0] == '-') {
913         $lines[0]++;
914         $class = 'removed';
915       } elseif ($line[0] == '+') {
916         $lines[1]++;
917         $class = 'added';
918       } else {
919         $lines[0]++;
920         $lines[1]++;
921       }
922     } else {
923       $lines[0]++;
924       $lines[1]++;
925     }
926     $row = "<tr class='$class";
927     if ($first) {
928       $row .= ' first';
929     }
930     if ($class != 'meta' && $first) {
931       $first = false;
932     }
933     $row .= "'>";
934
935     switch ($class) {
936       case 'meta':
937         $line_info = '';
938         $row .= "<td class='lineno'></td><td class='lineno'></td>";
939         break;
940       case 'added':
941         $row .= "<td class='lineno'></td><td class='lineno'>" . $lines[1] . "</td>";
942         break;
943       case 'removed':
944         $row .= "<td class='lineno'>" . $lines[0] . "</td><td class='lineno'></td>";
945         break;
946       default:
947         $row .= "<td class='lineno'>" . $lines[0] . "</td><td class='lineno'>" . $lines[1] . "</td>";
948     }
949     $anchor = $abase . '.' . $nlines;
950     $row .= "<td class='linelink'><a name='$anchor'></a><a href='#$anchor' title='link to this line'>#</a></td>";
951
952     $line = htmlspecialchars($line, ENT_QUOTES, 'utf-8');
953     $row .= "<td class='line' width='100%'>$line</td></tr>\n";
954     $html .= $row;
955
956     if (!count($diffstr)) {
957       break;
958     }
959     $line = array_shift($diffstr);
960     $nlines++;
961   }
962
963   if ($nlines == 0) {
964     return null;
965   }
966
967   $html .= "</table>";
968   return $html;
969 }
970
971 function mtrack_mime_detect($filename, $namehint = null)
972 {
973   /* does config tell us how to decide mimetype */
974   $detector = MTrackConfig::get('core', 'mimetype_detect');
975
976   /* if detector is blank, we'll try to figure out which one to use */
977   if (empty($detector)) {
978     if (function_exists('finfo_open')) {
979       $detector = 'fileinfo';
980     } elseif (function_exists('mime_content_type')) {
981       $detector = 'mime_magic';
982     } else {
983       $detector = 'file';
984     }
985   }
986
987   /* use detector or all mimetypes will be blank */
988   if ($detector === 'fileinfo') {
989     if (defined('FILEINFO_MIME_TYPE')) {
990       $fileinfo = finfo_open(FILEINFO_MIME_TYPE);
991     } else {
992       $magic = MTrackConfig::get('core', 'mime.magic');
993       if (strlen($magic)) {
994         $fileinfo = finfo_open(FILEINFO_MIME, $magic);
995       } else {
996         $fileinfo = finfo_open(FILEINFO_MIME);
997       }
998     }
999     $mimetype = finfo_file($fileinfo, $filename);
1000     finfo_close($fileinfo);
1001   } elseif ($detector === 'mime_magic') {
1002     $mimetype = mime_content_type($filename);
1003   } elseif (PHP_OS != 'SunOS') {
1004     $mimetype = shell_exec("file -b --mime " . escapeshellarg($filename));
1005   } else {
1006     $mimetype = 'application/octet-stream';
1007   }
1008   $mimetype = trim(preg_replace("/\s*;.*$/", '', $mimetype));
1009   if (empty($mimetype)) {
1010     $mimetype = 'application/octet-stream';
1011   }
1012   if ($mimetype == 'application/octet-stream') {
1013     if ($namehint === null) {
1014       $namehint = $filename;
1015     }
1016     $pi = pathinfo($namehint);
1017     switch (strtolower($pi['extension'])) {
1018       case 'bin': return 'application/octet-stream';
1019       case 'exe': return 'application/octet-stream';
1020       case 'dll': return 'application/octet-stream';
1021       case 'iso': return 'application/octet-stream';
1022       case 'so': return 'application/octet-stream';
1023       case 'a': return 'application/octet-stream';
1024       case 'lib': return 'application/octet-stream';
1025       case 'pdf': return 'application/pdf';
1026       case 'ps': return 'application/postscript';
1027       case 'ai': return 'application/postscript';
1028       case 'eps': return 'application/postscript';
1029       case 'ppt': return 'application/vnd.ms-powerpoint';
1030       case 'xls': return 'application/vnd.ms-excel';
1031       case 'tiff': return 'image/tiff';
1032       case 'tif': return 'image/tiff';
1033       case 'wbmp': return 'image/vnd.wap.wbmp';
1034       case 'png': return 'image/png';
1035       case 'gif': return 'image/gif';
1036       case 'jpg': return 'image/jpeg';
1037       case 'jpeg': return 'image/jpeg';
1038       case 'ico': return 'image/x-icon';
1039       case 'bmp': return 'image/bmp';
1040       case 'css': return 'text/css';
1041       case 'htm': return 'text/html';
1042       case 'html': return 'text/html';
1043       case 'txt': return 'text/plain';
1044       case 'xml': return 'text/xml';
1045       case 'eml': return 'message/rfc822';
1046       case 'asc': return 'text/plain';
1047       case 'rtf': return 'application/rtf';
1048       case 'wml': return 'text/vnd.wap.wml';
1049       case 'wmls': return 'text/vnd.wap.wmlscript';
1050       case 'gtar': return 'application/x-gtar';
1051       case 'gz': return 'application/x-gzip';
1052       case 'tgz': return 'application/x-gzip';
1053       case 'tar': return 'application/x-tar';
1054       case 'zip': return 'application/zip';
1055       case 'sql': return 'text/plain';
1056     }
1057     // if the file is ascii, then treat it as text/plain
1058     $fp = fopen($filename, 'rb');
1059     $mimetype = 'text/plain';
1060     do {
1061       $x = fread($fp, 8192);
1062       if (!strlen($x)) break;
1063       if (preg_match('/([\x80-\xff])/', $x, $M)) {
1064         $mimetype = 'application/octet-stream';
1065         break;
1066       }
1067     } while (true);
1068     $fp = null;
1069   }
1070   return $mimetype;
1071 }
1072
1073 function mtrack_run_tool($toolname, $mode, $args = null)
1074 {
1075   global $FORKS;
1076
1077   $tool = MTrackConfig::get('tools', $toolname);
1078   if (!strlen($tool)) {
1079     $tool = $toolname;
1080   }
1081   if (PHP_OS == 'Windows' && strpos($tool, ' ') !== false) {
1082     $tool = '"' . $tool . '"';
1083   }
1084   $cmd = $tool;
1085   if (is_array($args)) {
1086     foreach ($args as $arg) {
1087       if (is_array($arg)) {
1088         foreach ($arg as $a) {
1089           $cmd .= ' ' . escapeshellarg($a);
1090         }
1091       } else {
1092         $cmd .= ' ' . escapeshellarg($arg);
1093       }
1094     }
1095   }
1096   if (!isset($FORKS[$cmd])) {
1097     $FORKS[$cmd] = 0;
1098   }
1099   $FORKS[$cmd]++;
1100   if (false) {
1101     if (php_sapi_name() == 'cli') {
1102       echo $cmd, "\n";
1103     } else {
1104       error_log($cmd);
1105       echo htmlentities($cmd) . "<br>\n";
1106     }
1107   }
1108
1109   switch ($mode) {
1110     case 'read':   return popen($cmd, 'r');
1111     case 'write':  return popen($cmd, 'w');
1112     case 'string': return stream_get_contents(popen($cmd, 'r'));
1113     case 'proc':
1114       $pipedef = array(
1115         0 => array('pipe', 'r'),
1116         1 => array('pipe', 'w'),
1117         2 => array('pipe', 'w'),
1118       );
1119       $proc = proc_open($cmd, $pipedef, $pipes);
1120       return array($proc, $pipes);
1121   }
1122 }
1123
1124 if (php_sapi_name() != 'cli') {
1125   set_exception_handler('mtrack_last_chance_saloon');
1126   error_reporting(E_NOTICE|E_ERROR|E_WARNING);
1127   ini_set('display_errors', false);
1128   set_time_limit(300);
1129 }
1130
1131