<?php if (!defined('PmWiki')) exit();
/*  Copyright 2004 Patrick R. Michaud (pmichaud@pobox.com)
    This file is part of PITS (PmWiki Issue Tracking System), you can
    redistribute it and/or modify it under the terms of the GNU General
    Public License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This file defines code needed for the PmWiki Issue Tracking System
    (PITS).  The code is highly specialized towards the needs of
    pmwiki.org, and may not be useful (or even usable) anywhere else,
    but enough people have asked for it that I'm making it available
    in the Cookbook and wishing people the best of luck with it.

    Eventually there will likely be a "supported" version of this
    capability, and the supported version *may* even use the code below :-).
    But until Pm clears a few other items off of his to-do list, he
    may have limited ability to provide detailed support.  (Offers of
    financial support have been known to significantly affect his
    development/support priorities, however. :)

    Issues with PITS can, of course, be registered in PITS on
    pmwiki.org (http://www.pmwiki.org/PITS).

    Okay, enough disclaimers, here's the code already.

    - ~Pm

    ----

	A little bit later, Didier Lebrun, who was looking for this sort of
	application for a project, evaluated that PITS code could be quite
	easily de-specialized by moving the hardcoded stuff specializing it
	into configuration vars. So he did.

	The configuration is done mainly through template vars, with original
	PITS config being the default. Pm's original PITS script has only been
	changed when necessary, for the single purpose of making things
	configurable. The templates are as raw as possible, so as to be able
	to configure virtually anything while introducing as little overhead
	as possible. The de-specialized version should be able to run PITS as
	well as any other application of the same kind.

    - ~DidierLebrun

    ----

    Usage:
    * Copy pits-*.php into cookbook/pits.php
    * Create a local/MyPitsGroup.php file
    * Put in it the vars you want to ajust (see Config vars below)
    * At the end of it, put:
      <?include_once("cookbook/pits.php");      
    * Create a MyPitsGroup.MyPitsForm page and put in it:
      (:pitsform:)
    * Create a MyPitsGroup.MyPitsList page and put in it:
      (:pitslist[ somefield=somevalue,othervalue otherfield=!excludevalue ...]:)
      where "somefield" is the name of a field and "somevalue,othervalue"
      are the values you want to list. On the opposite "otherfield=!excludevalue"
      will exclude the records where "otherfield" correspond to "excludevalue".
      But you can just put (:pitslist:) to display the full list.
    * ... That's it !

    ----

    Notes:
    * All vars in the templates MUST be escaped (ex: \$SomeVar)
    * Vars can be "local" ones or "global" ones
    * Local vars correspond to field names:
      (ex: $summary = content of the 'summary' field)
    * Global vars are those given by PmWiki, like $Author and $Now.
      Currently, $Author and $Now are the only ones supported, but you
      can add others in function PitsEvalTemplate(). You must avoid
      $...Url vars like $PageUrl and such, since the http://... link in
      them would be interpretated twice. But you can use [[...]] type of
      links anyway.
    * You can even use PHP functions in templates:
      ex: ".strftime('%Y-%m-%d %H:%M',$Now)." (see $PitsFormTemplateFmt)

    ----

    History:
    0.10 - Initial version released by Pm with the disclaimers above
    0.20 - Modifications by Didier Lebrun <dl@vaour.net>:
           * De-specialized the original code (hardcoded -> config vars)
           * Stripped $PageUrl vars in template
           * Allowed space after label ("Label : content")
           * Limited parsing to the PITS header (leave out the comments below)
           * Allowed links in any table fields (ex: [[~Author]])
           * Stripped LF and other spaces between HTML lines (avoid vspace)
           * Added HTML entities charset for some systems (mainly NTFS ?)
    0.21 - Modifications by Didier Lebrun <dl@vaour.net>:
           * Fixed label parsing for i18n charsets
           * Fixed description entry in edit template (LF problem)
           * Transfered default values from edit template to form template
           * Suppressed unuseful $PitsGroup global var
           * Added (:pitstrail[ PageName|Label]:) markup and assorted function
    0.22 - Modification by Fabrice Mousset <fabrice.mousset@laposte.net>
           * Fixed (:pitlist:) makeup (directive ==> directives)
           * Fixed misspelled variable $PitsIDLenght ==> $PitsIDLength
           * Fixed PITS page list to add $PitsIDLength into scan pattern
           * Fixed FmtPitsList() function when no PITS are defined (deadlock)
           * Fixed PitsTrail() function when less than 2 PITS are defined (deadlock)
    0.23 - Modification by Fabrice Mousset <fabrice.mousset@laposte.net>
           * Fixed html code output to remove '\' miss placed
           * Added $PitsNoItemText variable to personalize output when no PITS present
*/

define(PITS_VERSION, '0.23');

# ---- Config vars ---- #

## These vars can be overriden in the PITS group configuration file
## (local/YourPITSGroupName.php). Whenever you see something like:
##   SDV($SomeVar, "default setup");
## you must put in your config something like:
##   $SomeVar = "custom setup";
## SDV() is a just PmWiki function allowing to set a default value
## when none has been setup in the config.

## Number of chars for page names
SDV($PitsIDLength, 5);

## Template for empty PITS list
SDV($PitsNoItemText, "No bugs ;-)");

## Label to field name mapping. Labels and names can be whatever you like,
## as long as they are consistent through all configuration vars.
SDV($PitsLabels, array(
	'Summary' => 'summary',
	'Created' => 'created',
	'Status' => 'status',
	'Category' => 'category',
	'From' => 'author',
	'Assigned' => 'assigned',
	'Priority' => 'priority',
	'Version' => 'version',
	'OS' => 'os',
	'Description' => 'description'
));

## Template for the input form
SDV($PitsFormTemplateFmt, "
<form method='post'>
	<input type='hidden' name='action' value='postpits' />
	<input type='hidden' name='created' value='" . strftime('%Y-%m-%d %H:%M', $Now) . "' />
	<input type='hidden' name='status' value='Open' />
	<table class='pitsform'>
		<tr>
			<td class='label'>Author:</td>
			<td class='input'><input type='text' name='author' value='\$Author' /></td>
		</tr>
		<tr>
			<td class='label'>Summary:</td>
			<td class='input'><input type='text' name='summary' size='60' /></td>
		</tr>
		<tr>
			<td class='label'>Category:</td>
			<td class='input'>
				<select name='category'>
					<option value=''></option>
					<option value='Bug'>Bug</option>
					<option value='Feature'>Feature/Change Request</option>
					<option value='Documentation'>Documentation</option>
					<option value='Cookbook'>Cookbook</option>
					<option value='PHP Compatibility'>PHP Compatibility</option>
					<option value='Other'>Other</option>
				</select>
			</td>
		</tr>
		<tr>
			<td class='label'>Priority:</td>
			<td class='input'>
				Low
				<input type='radio' name='priority' value='1' />
				<input type='radio' name='priority' value='2' />
				<input type='radio' name='priority' value='3' />
				<input type='radio' name='priority' value='4' />
				<input type='radio' name='priority' value='5' />
				High
			</td>
		</tr>
		<tr>
			<td class='label'>PmWiki Version:</td>
			<td class='input'><input type='text' name='version' /></td>
		</tr>
		<tr>
			<td class='label'>OS/Webserver/<br />PHP Version:</td>
			<td class='input'><input type='text' name='os' /></td>
		</tr>
		<tr>
			<td class='label' valign='top'>Description:</td>
			<td class='input'><textarea name='description' cols='60' rows='15'></textarea></td>
		</tr>
	</table>
	<div align='center'><input type='submit' value='Submit new issue' accesskey='s' /></div>
</form>
");

## Template for filling the edit box with input data
## "----" MUST be present at the end to separate the entries from the
## eventual comments that could be added afterwards.
SDV($PitsEditTemplateFmt, "
Summary: \$summary
Created: \$created
Status: \$status
Category: \$category
From: [[~\$author]]
Assigned: \$assigned
Priority: \$priority

Version: \$version
##OS: os (changed 2013/05/04) to OS: \$os
OS: \$os

Description:[[<<]]\$description

----

!!!Comments

----
(:pitstrail:)
");

## Template for list table.
## The "(/)PitsListItemFmt" tags MUST be present as delimiters of the entry part.
SDV($PitsListTemplateFmt, "
<table border='1' cellspacing='0' class='pitslist'>
	<tr>
		<th><a href='?order=-name'>Issue#</a></th>
		<th><a href='?order=created'>Created</a></th>
		<th><a href='?order=category'>Category</a></th>
		<th><a href='?order=version'>Version</a></th>
		<th><a href='?order=-priority'>Priority</a></th>
		<th><a href='?order=status'>Status</a></th>
		<th><a href='?order=summary'>Summary</a></th>
	</tr>
<!--PitsListItemFmt-->
	<tr>
		<td>[[\$name]]</td>
		<td>\$created</td>
		<td>\$category</td>
		<td>\$version</td>
		<td>\$priority</td>
		<td>\$status</td>
		<td>\$summary</td>
	</tr>
<!--/PitsListItemFmt-->
</table>
");

## HTML styles
SDV($PitsStylesFmt, "
.pitsform {}
.pitsform td.label { text-align:right; font-weight:bold; }
.pitsform td.input {}
.pitspage {}
.pitspage span.label {}
.pitspage span.data {}
.pitslist {}
.pitslist th { background-color:#eeeeee; }
.pitslist th a { text-decoration:none; }
");

## Edit form top message
SDV($PitsEditMessageFmt, "<p class='vspace'>Please review and make any edits
    to your issue below, then press 'Save'.</p>");

## Fields that can be filtered by name=criteria args
SDV($PitsListFilters, array('summary','created','status','category','priority','version'));

## Criteria used for default and complementary sorting after explicit criteria
SDV($PitsListDefaultSort, array('-priority','status','category','name'));

## Charset for HTML entities conversion ('ISO-8859-1' usually, 'UTF-8' on NTFS systems)
SDV($PitsHtmlEntitiesCharset, 'ISO-8859-1');

# ---- end of Config vars ---- #

$HTMLStylesFmt[] = $PitsStylesFmt;
markup('pitsform', 'inline', '/\\(:pitsform:\\)/e', "Keep(PitsForm(\$pagename))");

markup('pitslist', 'directives', '/\\(:pitslist\\s*(.*?):\\)/e',
  "FmtPitsList('', \$pagename, array('q' => PSS('$1')))");

markup('pitsread', 'directives', '/^('.implode('|', array_keys($PitsLabels)) . ')\s*:(.*)/',
  "<:block><div class='pitspage'><span class='label'>$1:</span>
  <span class='data'>$2</span></div>");

markup('pitstrail', '<links', '/\\(:pitstrail\\s*(.*?):\\)/e', "PitsTrail(\$pagename,'$1')");

include_once("$FarmD/scripts/author.php");

## PitsForm() generates the form for entering a new issue.  Note that
## once an issue has been created, it's a normal wikipage and is edited
## according to the normal editing code (i.e., there's no form-based
## editing yet).
function PitsForm($pagename)
{
  global $PitsFormTemplateFmt;

  $tmpl = preg_replace('/\s*\n\s*/', '', $PitsFormTemplateFmt);
  $out = PitsEvalTemplate(array(), $tmpl);
  return FmtPageName($out, $pagename);
}

## Fills the edit form with formatted input
if ($action == 'postpits')
{
  list($group) = explode('.', $pagename);
  Lock(2);
  # $pitslist = ListPages("/^$group\\.\\d/");
  $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/");
  $issue = 0;
  foreach($pitslist as $i)
    $issue = max(@$issue, substr($i, strlen($group) + 1));
  $pagename = sprintf("$group.%0" . $PitsIDLength . "d", @$issue + 1);
  $action = 'edit';
  $EditMessageFmt = $PitsEditMessageFmt;
  $_REQUEST['post'] = 1;
  $_POST['text'] = PitsEvalTemplate($_REQUEST, $PitsEditTemplateFmt);
}

## FmtPitsList() creates a table of PITS issues according to various
## criteria.
function FmtPitsList($fmt, $pagename, $opt)
{
  global $PitsListTemplateFmt, $PitsLabels, $PitsIDLength, $PitsHtmlEntitiesCharset, $PitsListFilters, $PitsListDefaultSort, $PitsNoItemText;

  list($group) = explode('.',$pagename);
  $opt = array_merge($opt, @$_REQUEST);
  $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/");
  ## If not PIT available, display default contents
  if(count($pitslist) < 1)
  {
    return FmtPageName("$PitsNoItemText", $pagename);
  }
  
  $tmpl = preg_replace('/\s*\n\s*/', '', $PitsListTemplateFmt);
  $sect = preg_split('#[[<]!--(/?(?:PitsListItemFmt)\\s?.*?)--[]>]#', $tmpl);
  if(count($sect) != 3) 
    Abort("\$PitsListTemplateFmt MUST have 3 sections !");
  $out[] = FmtPageName(array_shift($sect), $pagename);
  $terms = preg_split('/((?<!\\S)[-+]?[\'"].*?[\'"](?!\\S)|\\S+)/', $opt['q'], -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  foreach($terms as $t) 
  {
    if (trim($t) == '')
      continue;
    if (preg_match('/([^\'":=]*)[:=]([\'"]?)(.*?)\\2$/', $t,$match))
      $opt[strtolower($match[1])] = $match[3];
  }
  $n = 0; $plist = array();
  foreach($pitslist as $p)
  {
    $page = ReadPage($p);
    list($head) = preg_split('/\n----/', $page['text'], 0);
    preg_match_all('/(^|\n)([^:]*):(\s*\\[\\[<<\\]\\])?([^\n]*)/', $head,$match);
    $fields = array();
    for($i = 0; $i < count($match[2]); $i++)
      $fields[$PitsLabels[trim($match[2][$i])]] = htmlentities(trim($match[4][$i]), ENT_QUOTES, $PitsHtmlEntitiesCharset);
    foreach($PitsListFilters as $h)
    {
      if (!@$opt[$h])
        continue;
      foreach(preg_split('/[ ,]/', $opt[$h]) as $t)
      {
        if (substr($t, 0, 1) != '-' && substr($t, 0, 1) != '!')
        {
          if (strpos(strtolower(@$fields[$h]), strtolower($t)) === false)
            continue 3;
        } else if (strpos(strtolower(@$fields[$h]), strtolower(substr($t,1))) !== false)
          continue 3;
      }
    }
    $plist[$n] = $fields;
    list(, $plist[$n]['name']) = explode('.', $p);
    $n++;
  }
  
  if($n == 0)
  {
    return FmtPageName($PitsNoItemText, $pagename);
  }

  $cmp = CreateOrderFunction(implode(',', array_merge(array(@$opt['order']), (array)$PitsListDefaultSort)));
  usort($plist, $cmp);
  $tr = array_shift($sect);
  foreach($plist as $p) {
    $out[] = PitsEvalTemplate($p,$tr);
  }
             
  $out[] = array_shift($sect);
  return FmtPageName(implode('', $out), $pagename);
}

## This function creates a WikiTrail based on the current page number
function PitsTrail($pagename, $trailindex='')
{
  global $PitsIDLength;
  
  list($group, $name) = explode('.', $pagename);
  trim($trailindex);
  if(empty($trailindex)) 
    $trailindex = $group;
  $trailindex = '[[' . $trailindex . ']]';
  # $pitslist = ListPages("/^$group\\.\\d+$/");
  $pitslist = ListPages("/^$group\\.\\d{" . $PitsIDLength . "}$/");
    
  ## There must be at least 2 PITS to display the trail!
  if(count($pitslist) < 2)
    return "";
    
  sort($pitslist);
  $i = @$name-1;
  list(,$min) = explode('.', array_shift($pitslist));
  for($i; $i >= @$min; $i--)
  {
    $prev = sprintf("%0" . $PitsIDLength . "d", @$i);
    if(PageExists("$group.$prev"))
    { 
      $prev = '[[' . $prev . ']]'; 
      break; 
    }
    $prev = '';
  }
  $i = @$name+1;
  list(,$max) = explode('.', array_pop($pitslist));
  for($i; $i <= @$max; $i++) 
  {
    $next = sprintf("%0" . $PitsIDLength . "d", @$i);
    if(PageExists("$group.$next"))
    {
      $next = '[[' . $next . ']]'; 
      break; 
    }
    $next = '';
  }
  return "<span class='wikitrail'>&lt;&lt; $prev | $trailindex | $next &gt;&gt;</span>";
}

## This function creates specialized ordering functions needed to
## (more efficiently) perform sorts on arbitrary sets of criteria.
function CreateOrderFunction($order)
{
  $code = '';
  foreach(preg_split('/[\\s,|]+/', strtolower($order), -1, PREG_SPLIT_NO_EMPTY) as $o)
  {
    if (substr($o, 0, 1) == '-')
    { 
      $r = '-';
      $o = substr($o, 1);
    }
    else 
      $r='';
    if (preg_match('/\\W/', $o))
      continue;
    $code .= "\$c=strcasecmp(@\$x['$o'],@\$y['$o']); if (\$c!=0) return $r\$c;\n";
  }
  $code .= "return 0;\n";
  return create_function('$x,$y', $code);
}

## This function is used to evaluate the templates while insulating their
## vars from potential name conflicts
function PitsEvalTemplate($hash, $tmpl)
{
  global $Author,$Now;
  foreach($hash as $k=>$v) 
    $$k = $v;
  return(@eval("return(\"$tmpl\");"));
}

?>