/*
** Copyright (c) 2002 D. Richard Hipp
**
** This program is free software; 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 program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
** General Public License for more details.
** 
** You should have received a copy of the GNU General Public
** License along with this library; if not, write to the
** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
** Boston, MA  02111-1307, USA.
**
** Author contact information:
**   drh@hwaci.com
**   http://www.hwaci.com/drh/
**
*******************************************************************************
**
** This file contains code used to generate web pages for
** processing trouble and enhancement tickets.
*/
#include "config.h"
#include "ticket.h"
#include <time.h>

/*
** If the "notify" configuration parameter exists in the CONFIG
** table and is not an empty string, then make various % substitutions
** on that string and execute the result.
*/
void ticket_notify(int tn, int first_change, int last_change, int atn){
  const char *zNotify;
  char *zCmd;
  int i, j, c;
  int cmdSize;
  int cnt[128];
  const char *azSubst[128];

  static const struct { int key; char *zColumn; } aKeys[] = {
      { 'a',  "assignedto"  },
      /* A - e-mail address of assignedto person */
      { 'c',  "contact"     },
      { 'd',  "description" },
      /* D - description, HTML formatted */
      /* f - First TKTCHNG rowid of change set; zero if new record */
      /* h = attacHment number if change is a new attachment; zero otherwise */
      /* l - Last TKTCHNG rowid of change set; zero if new record */
      /* n - ticket number */
      /* p - project name  */
      { 'r',  "remarks"     },
      /* R - remarks, HTML formatted */
      { 's',  "status"      },
      { 't',  "title"       },
      /* u - current user  */
      { 'w',  "owner"       },
      { 'y',  "type"        },
      { '1',  "extra1"        },
      { '2',  "extra2"        },
      { '3',  "extra3"        },
      { '4',  "extra4"        },
      { '5',  "extra5"        },
  };

  zNotify = db_config("notify",0);
  if( zNotify==0 || zNotify[0]==0 ) return;
  memset(cnt, 0, sizeof(cnt));
  memset(azSubst, 0, sizeof(azSubst));
  for(i=0; zNotify[i]; i++){
    if( zNotify[i]=='%' ){
      c = zNotify[i+1] & 0x7f;
      cnt[c&0x7f]++;
    }
  }
  if( cnt['n']>0 ){
    azSubst['n'] = mprintf("%d", tn);
  }
  if( cnt['f']>0 ){
    azSubst['f'] = mprintf("%d", first_change);
  }
  if( cnt['h']>0 ){
    azSubst['h'] = mprintf("%d", atn);
  }
  if( cnt['l']>0 ){
    azSubst['l'] = mprintf("%d", last_change);
  }
  if( cnt['u']>0 ){
    azSubst['u'] = mprintf("%s", g.zUser);
  }
  if( cnt['p']>0 ){
    azSubst['p'] = mprintf("%s", g.zName);
  }
  if( cnt['D']>0 ){
    /* ensure we grab a description */
    cnt['d']++;
  }
  if( cnt['R']>0 ){
    /* ensure we grab remarks */
    cnt['r']++;
  }
  if( cnt['A']>0 ){
    azSubst['A'] = 
      db_short_query("SELECT user.email FROM ticket, user "
                     "WHERE ticket.tn=%d and ticket.assignedto=user.id", tn);
  }
  for(i=0; i<sizeof(aKeys)/sizeof(aKeys[0]); i++){
    c = aKeys[i].key;
    if( cnt[c]>0 ){
      azSubst[c] =
        db_short_query("SELECT %s FROM ticket WHERE tn=%d",
                       aKeys[i].zColumn, tn);
    }
  }
  if( cnt['c']>0 && azSubst['c'][0]==0 ){
    azSubst['c'] = 
      db_short_query("SELECT user.email FROM ticket, user "
                     "WHERE ticket.tn=%d and ticket.owner=user.id", tn);
  }
  if( cnt['D'] ){
    azSubst['D'] = format_formatted( azSubst['d'] );
    cnt['d']--;
  }
  if( cnt['R'] ){
    azSubst['R'] = format_formatted( azSubst['r'] );
    cnt['r']--;
  }

  /* Sanitize the strings to be substituted by removing any single-quotes
  ** and backslashes.
  **
  ** That way, the notify command can contains strings like '%d' or '%r'
  ** (surrounded by quotes) and a hostile user cannot insert arbitrary
  ** shell commands.  Also figure out how much space is needed to hold
  ** the string after substitutes occur.
  */
  cmdSize = strlen(zNotify)+1;
  for(i=0; i<sizeof(azSubst)/sizeof(azSubst[0]); i++){
    if( azSubst[i]==0 || cnt[i]<=0 ) continue;
    azSubst[i] = quotable_string(azSubst[i]);
    cmdSize += cnt[i]*strlen(azSubst[i]);
  }

  zCmd = malloc( cmdSize + 1 );
  if( zCmd==0 ) return;
  for(i=j=0; zNotify[i]; i++){
    if( zNotify[i]=='%' && (c = zNotify[i+1]&0x7f)!=0 && azSubst[c]!=0 ){
      int k;
      const char *z = azSubst[c];
      for(k=0; z[k]; k++){ zCmd[j++] = z[k]; }
      i++;
    }else{
      zCmd[j++] = zNotify[i];
    }
  }
  zCmd[j] = 0;
  assert( j<=cmdSize );
  system(zCmd);
  free(zCmd);
}

/*
** Adds all appropriate action bar links for ticket tools
*/
static void add_tkt_tools(
  const char *zExcept,
  int tn
){
  int i;
  char *zLink;
  char **azTools;
  db_add_functions();
  azTools = db_query("SELECT tool.name FROM tool,user "
                     "WHERE tool.object='tkt' AND user.id='%q' "
                     "      AND cap_and(tool.perms,user.capabilities)!=''",
                     g.zUser);

  for(i=0; azTools[i]; i++){
    if( zExcept && 0==strcmp(zExcept,azTools[i]) ) continue;

    zLink = mprintf("tkttool?t=%T&tn=%d", azTools[i], tn);
    common_add_action_item(zLink, azTools[i]);
  }
}

/*
** WEBPAGE: /tkttool
**
** Execute an external tool on a given ticket
*/
void tkttool(void){
  int tn = atoi(PD("tn","0"));
  const char *zTool = P("t");
  char *zAction;
  const char *azSubst[32];
  int n = 0;

  if( tn==0 || zTool==0 ) cgi_redirect("index");

  login_check_credentials();
  if( !g.okRead ){ login_needed(); return; }
  throttle(1,0);
  history_update(0);

  zAction = db_short_query("SELECT command FROM tool "
                           "WHERE name='%q'", zTool);
  if( zAction==0 || zAction[0]==0 ){
    cgi_redirect(mprintf("tktview?tn=%d",tn));
  }

  common_standard_menu(0, "search?t=1");
  common_add_action_item(mprintf("tktview?tn=%d", tn), "View");
  common_add_action_item(mprintf("tkthistory?tn=%d", tn), "History");
  add_tkt_tools(zTool,tn);

  common_header("#%d: %h", tn, zTool);

  azSubst[n++] = "TN";
  azSubst[n++] = mprintf("%d",tn);
  azSubst[n++] = 0;

  n = execute_tool(zTool,zAction,0,azSubst);
  free(zAction);
  if( n<=0 ){
    cgi_redirect(mprintf("tktview?tn=%d", tn));
  }
  common_footer();
}

/*
** WEBPAGE: /tktnew
**
** A web-page for entering a new ticket.
*/
void ticket_new(void){
  const char *zTitle = trim_string(PD("t",""));
  const char *zType = P("y");
  const char *zVers = PD("v","");
  const char *zDesc = remove_blank_lines(PD("d",""));
  const char *zContact = PD("c","");
  const char *zWho = P("w");
  const char *zSubsys = PD("s","");
  const char *zSev = PD("r",db_config("dflt_severity","1"));
  const char *zPri = PD("p",db_config("dflt_priority","1"));
  const char *zFrom = P("f");
  int isPreview = P("preview")!=0;
  int severity, priority;
  char **az;
  char *zErrMsg = 0;
  int i;

  login_check_credentials();
  if( !g.okNewTkt ){
    login_needed();
    return;
  }
  throttle(1,1);
  severity = atoi(zSev);
  priority = atoi(zPri);

  /* user can enter #tn or just tn, and empty is okay too */
  zFrom = extract_integer(zFrom);

  if( zType==0 ){
    zType = db_config("dflt_tkt_type","code");
  }
  if( zWho==0 ){
    zWho = db_config("assignto","");
  }
  if( zTitle && strlen(zTitle)>70 ){
    zErrMsg = "Please make the title no more than 70 characters long.";
  }
  if( zErrMsg==0 && zTitle[0] && zType[0] && zDesc[0] && P("submit")
      && (zContact[0] || !g.isAnon) ){
    int tn;
    time_t now;
    const char *zState;

    db_execute("BEGIN");
    az = db_query("SELECT max(tn)+1 FROM ticket");
    tn = atoi(az[0]);
    if( tn<=0 ) tn = 1;
    time(&now);
    zState = db_config("initial_state", "new");
    db_execute(
       "INSERT INTO ticket(tn, type, status, origtime,  changetime, "
       "                   version, assignedto, severity, priority, derivedfrom, "
       "                   subsystem, owner, title, description, contact) "
       "VALUES(%d,'%q','%q',%d,%d,'%q','%q',%d,%d,'%q','%q','%q','%q','%q','%q')",
       tn, zType, zState, now, now, zVers, zWho, severity, priority, zFrom, zSubsys,
       g.zUser, zTitle, zDesc, zContact
    );
    for(i=1; i<=5; i++){
      const char *zVal;
      char zX[3];
      bprintf(zX,sizeof(zX),"x%d",i);
      zVal = P(zX);
      if( zVal && zVal[0] ){
        db_execute("UPDATE ticket SET extra%d='%q' WHERE tn=%d", i, zVal, tn);
      }
    }
    db_execute("COMMIT");
    ticket_notify(tn, 0, 0, 0);
    cgi_redirect(mprintf("tktview?tn=%d",tn));
    return;
  }else if( P("submit") ){
    if( zTitle[0]==0 ){
      zErrMsg = "Please enter a title.";
    }else if( zDesc[0]==0 ){
      zErrMsg = "Please enter a description.";
    }else if( zContact[0]==0 && g.isAnon ){
      zErrMsg = "Please enter your contact information.";
    }
  }
  
  common_standard_menu("tktnew", 0);
  common_add_help_item("CvstracTicket");
  common_add_action_item( "index", "Cancel");
  common_header("Create A New Ticket");
  if( zErrMsg ){
    cgi_printf("<blockquote>\n"
           "<font color=\"red\">%h</font>\n"
           "</blockquote>\n",zErrMsg);
  }
  cgi_printf("<form action=\"%T\" method=\"POST\">\n"
         "<table cellpadding=\"5\">\n"
         "\n"
         "<tr>\n"
         "<td colspan=2>\n"
         "Enter a one-line summary of the problem:<br>\n"
         "<input type=\"text\" name=\"t\" size=70 maxlength=70 value=\"%h\">\n"
         "</td>\n"
         "</tr>\n"
         "\n"
         "<tr>\n"
         "<td align=\"right\">Type:\n",g.zPath,zTitle);
  cgi_v_optionmenu2(0, "y", zType, (const char**)db_query(
      "SELECT name, value FROM enums WHERE type='type'"));
  cgi_printf("</td>\n"
         "<td>What type of ticket is this?</td>\n"
         "</tr> \n"
         "\n"
         "<tr>\n"
         "  <td align=\"right\"><nobr>\n"
         "    Version: <input type=\"text\" name=\"v\" value=\"%h\" size=\"10\">\n"
         "  </nobr></td>\n"
         "  <td>\n"
         "     Enter the version and/or build number of the product\n"
         "     that exhibits the problem.\n"
         "  </td>\n"
         "</tr>\n"
         "\n"
         "<tr>\n"
         "  <td align=\"right\"><nobr>\n"
         "    Severity:\n",zVers);
  cgi_optionmenu(0, "r", zSev,
         "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0);
  cgi_printf("  </nobr></td>\n"
         "  <td>\n"
         "    How debilitating is the problem?  \"1\" is a show-stopper defect with\n"
         "    no workaround.  \"2\" is a major defect with a workaround.  \"3\"\n"
         "    is a mid-level defect.  \"4\" is an annoyance.  \"5\" is a cosmetic\n"
         "    defect or a nice-to-have feature request.\n"
         "  </td>\n"
         "</tr>\n"
         "\n"
         "<tr>\n"
         "  <td align=\"right\"><nobr>\n"
         "    Priority:\n");
  cgi_optionmenu(0, "p", zPri,
         "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0);
  cgi_printf("  </nobr></td>\n"
         "  <td>\n"
         "    How quickly do you need this ticket to be resolved?\n"
         "    \"1\" means immediately.\n"
         "    \"2\" means before the next build.  \n"
         "    \"3\" means before the next release.\n"
         "    \"4\" means implement as time permits.\n"
         "    \"5\" means defer indefinitely.\n"
         "  </td>\n"
         "</tr>\n"
         "\n");
  if( g.okWrite ){
    cgi_printf("<tr>\n"
           "  <td align=\"right\"><nobr>\n"
           "    Assigned To:\n");
    az = db_query("SELECT id FROM user UNION SELECT '' ORDER BY id");
    cgi_v_optionmenu(0, "w", zWho, (const char **)az);
    db_query_free(az);
    cgi_printf("  </nobr></td>\n"
           "  <td>\n"
           "    To what user should this problem be assigned?\n"
           "  </td>\n"
           "</tr>\n"
           "\n");
    az = db_query("SELECT '', '' UNION ALL "
            "SELECT name, value  FROM enums WHERE type='subsys'");
    if( az[0] && az[1] && az[2] ){
      cgi_printf("<tr>\n"
             "  <td align=\"right\"><nobr>\n"
             "    Subsystem:\n");
      cgi_v_optionmenu2(4, "s", zSubsys, (const char**)az);
      db_query_free(az);
      cgi_printf("  </nobr></td>\n"
             "  <td>\n"
             "    Which component is showing a problem?\n"
             "  </td>\n"
             "</tr>\n");
    }
  }
  cgi_printf("<tr>\n"
         "  <td align=\"right\"><nobr>\n"
         "    Derived From: <input type=\"text\" name=\"f\" value=\"%h\" size=\"5\">\n"
         "  </nobr></td>\n"
         "  <td>\n"
         "     Is this related to an existing ticket?\n"
         "  </td>\n"
         "</tr>\n",zFrom);
  if( g.isAnon ){
    cgi_printf("<tr>\n"
           "  <td align=\"right\"><nobr>\n"
           "    Contact: <input type=\"text\" name=\"c\" value=\"%h\" size=\"20\">\n"
           "  </nobr></td>\n"
           "  <td>\n"
           "     Enter a phone number or e-mail address where a developer can\n"
           "     contact you with questions about this ticket.  The information\n"
           "     you enter will be available to the developers only and will not\n"
           "     be visible to general users.\n"
           "  </td>\n"
           "</tr>\n"
           "\n",zContact);
  }
  for(i=1; i<=5; i++){
    char **az;
    const char *zDesc;
    const char *zName;
    char zX[3];
    char zExName[100];

    bprintf(zExName,sizeof(zExName),"extra%d_desc",i);
    zDesc = db_config(zExName, 0);
    if( zDesc==0 ) continue;
    bprintf(zExName,sizeof(zExName),"extra%d_name",i);
    zName = db_config(zExName, 0);
    if( zName==0 ) continue;
    az = db_query("SELECT name, value FROM enums "
                   "WHERE type='extra%d'", i);
    bprintf(zX, sizeof(zX), "x%d", i);
    cgi_printf("<tr>\n"
           "  <td align=\"right\"><nobr>\n"
           "    %h:\n",zName);
    if( az==0 || az[0]==0 ){
      cgi_printf("    <input type=\"text\" name=\"%h\" value=\"%h\" size=\"20\">\n",zX,PD(zX,""));
    }else{
      cgi_v_optionmenu2(0, zX, PD(zX,az[0]), (const char**)az);
    }
    cgi_printf("  </nobr></td>\n"
           "  <td>\n");
    /* description is already HTML markup */
    cgi_printf("     %s\n"
           "  </td>\n"
           "</tr>\n"
           "\n",zDesc);
  }
  cgi_printf("<tr>\n"
         "  <td colspan=\"2\">\n"
         "    Enter a detailed description of the problem.  For code defects,\n"
         "    be sure to provide details on exactly how the problem can be\n"
         "    reproduced.  Provide as much detail as possible. \n"
         "    <a href=\"#format_hints\">Formatting hints</a>.\n"
         "    <br>\n"
         "<textarea rows=\"10\" cols=\"70\" wrap=\"virtual\" name=\"d\">%h</textarea>\n",zDesc);
  if( isPreview ){
    cgi_printf("    <br>Description Preview:\n"
           "    <table border=1 cellpadding=15 width=\"100%%\"><tr><td>\n");
    output_formatted(zDesc, 0);
    cgi_printf("    </td></tr></table>\n");
  }
  if( g.okWrite ){
    cgi_printf("    <br>Note: If you want to include a large script or binary file\n"
           "    with this ticket you will be given an opportunity to add attachments\n"
           "    to the ticket after the ticket has been created.  Do not paste\n"
           "    large scripts or screen dumps in the description.\n");
  }
  cgi_printf("  </td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">\n"
         "    <input type=\"submit\" name=\"preview\" value=\"Preview\">\n"
         "  </td>\n"
         "  <td>\n"
         "    Preview the formatting of the description.\n"
         "  </td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">\n"
         "    <input type=\"submit\" name=\"submit\" value=\"Submit\">\n"
         "  </td>\n"
         "  <td>\n"
         "    After filling in the information about, press this button to create\n"
         "    the new ticket.\n"
         "  </td>\n"
         "</tr>\n"
         "</table>\n"
         "</form>\n"
         "<a name=\"format_hints\">\n"
         "<hr>\n"
         "<h3>Formatting Hints:</h3>\n");
  append_formatting_hints();
  common_footer();
}

/*
** Return TRUE if it is ok to undo a ticket change that occurred at
** chngTime and was made by user zUser.
**
** A ticket change can be undone by:
**
**    *  The Setup user at any time.
**
**    *  By the registered user who made the change within 24 hours of
**       the change.
**
**    *  By the Delete user within 24 hours of the change if the change
**       was made by anonymous.
*/
static int ok_to_undo_change(int chngTime, const char *zUser){
  if( g.okSetup ){
    return 1;
  }
  if( g.isAnon || chngTime<time(0)-86400 ){
    return 0;
  }
  if( strcmp(g.zUser,zUser)==0 ){
    return 1;
  }
  if( g.okDelete && strcmp(zUser,"anonymous")==0 ){
    return 1;
  }
  return 0;
}

/*
** WEBPAGE: /tktundo
**
** A webpage removing a prior edit to a ticket
*/
void ticket_undo(void){
  int tn = 0;
  const char *zUser;
  time_t tm;
  const char *z;
  char **az;
  int i;

  login_check_credentials();
  if( !g.okWrite ){ login_needed(); return; }
  throttle(1,1);
  tn = atoi(PD("tn","-1"));
  zUser = PD("u","");
  tm = atoi(PD("t","0"));
  if( tn<0 || tm==0 || zUser[0]==0 ){ cgi_redirect("index"); return; }
  if( !ok_to_undo_change(tm, zUser) ){
    goto undo_finished;
  }
  if( P("can") ){
    /* user cancelled */
    goto undo_finished;
  }
  if( P("w")==0 ){
    common_standard_menu(0,0);
    common_add_help_item("CvstracTicket");
    common_add_action_item(mprintf("tkthistory?tn=%d",tn), "Cancel");
    common_header("Undo Change To Ticket?");
    cgi_printf("<p>If you really want to remove the last edit to ticket #%d\n"
           "then click on the \"OK\" link below.  Otherwise, click on \"Cancel\".</p>\n"
           "<form method=\"GET\" action=\"tktundo\">\n"
           "<input type=\"hidden\" name=\"tn\" value=\"%d\">\n"
           "<input type=\"hidden\" name=\"u\" value=\"%t\">\n"
           "<input type=\"hidden\" name=\"t\" value=\"%d\">\n"
           "<table cellpadding=\"30\">\n"
           "<tr><td>\n"
           "<input type=\"submit\" name=\"w\" value=\"OK\">\n"
           "</td><td>\n"
           "<input type=\"submit\" name=\"can\" value=\"Cancel\">\n"
           "</td></tr>\n"
           "</table>\n"
           "</form>\n",tn,tn,zUser,tm);
    common_footer();
    return;
  }

  /* Make sure the change we are requested to undo is the vary last
  ** change.
  */
  z = db_short_query("SELECT max(chngtime) FROM tktchng WHERE tn=%d", tn);
  if( z==0 || tm!=atoi(z) ){
    goto undo_finished;
  }

  /* If we get this far, it means the user has confirmed that they
  ** want to undo the last change to the ticket.
  */
  db_execute("BEGIN");
  az = db_query("SELECT fieldid, oldval FROM tktchng "
                "WHERE tn=%d AND user='%q' AND chngtime=%d",
                tn, zUser, tm);
  for(i=0; az[i]; i+=2){
    db_execute("UPDATE ticket SET %s='%q' WHERE tn=%d", az[i], az[i+1], tn);
  }
  db_execute("DELETE FROM tktchng WHERE tn=%d AND user='%q' AND chngtime=%d",
             tn, zUser, tm);
  db_execute("COMMIT");

undo_finished:
  cgi_redirect(mprintf("tkthistory?tn=%d",tn));
}  


/*
** Extract the ticket number and report number from the "tn" query
** parameter.
*/
#if 0 /* NOT USED */
static void extract_codes(int *pTn, int *pRn){
  *pTn = *pRn = 0;
  sscanf(PD("tn",""), "%d,%d", pTn, pRn);
}
#endif

static void output_tkt_chng(char **azChng){
  time_t thisDate;
  struct tm *pTm;
  char zDate[100];
  char zPrefix[200];
  char zSuffix[100];
  char *z;
  const char *zType = (atoi(azChng[5])==0) ? "Check-in" : "Milestone";

  thisDate = atoi(azChng[0]);
  pTm = localtime(&thisDate);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm);
  if( azChng[2][0] ){
    bprintf(zPrefix, sizeof(zPrefix), "%h [%.20h] on branch %.50h: ",
            zType, azChng[1], azChng[2]);
  }else{
    bprintf(zPrefix, sizeof(zPrefix), "%h [%.20h]: ", zType, azChng[1]);
  }
  bprintf(zSuffix, sizeof(zSuffix), " (By %.30h)", azChng[3]);
  cgi_printf("<tr><td valign=\"top\" width=160 align=\"right\">%h</td>\n"
         "<td valign=\"top\" width=30 align=\"center\">\n",zDate);
  common_icon("dot");
  cgi_printf("</td>\n"
         "<td valign=\"top\" align=\"left\"> \n");
  output_formatted(zPrefix, 0);
  z = azChng[4];
  if( output_trim_message(z, MN_CKIN_MSG, MX_CKIN_MSG) ){
    output_formatted(z, 0);
    cgi_printf("&nbsp;[...]\n");
  }else{
    output_formatted(z, 0);
  }
  output_formatted(zSuffix, 0);
  cgi_printf("</td></tr>\n");
}

/*
** WEBPAGE: /tktview
**
** A webpage for viewing the details of a ticket
*/
void ticket_view(void){
  int i, j, nChng;
  int tn = 0, rn = 0;
  char **az;
  char **azChng;
  char **azDrv;
  char *z;
  const char *azExtra[5];
  char zPage[30];
  const char *zTn;

  login_check_credentials();
  if( !g.okRead ){ login_needed(); return; }
  throttle(1,0);
  history_update(0);
  zTn = PD("tn","");
  sscanf(zTn, "%d,%d", &tn, &rn);
  if( tn<=0 ){ cgi_redirect("index"); return; }
  bprintf(zPage,sizeof(zPage),"%d",tn);
  common_standard_menu("tktview", "search?t=1");
  if( rn>0 ){
    common_replace_nav_item(mprintf("rptview?rn=%d", rn), "Report");
    common_add_action_item(mprintf("tkthistory?tn=%d,%d", tn, rn), "History");
  }else{
    common_add_action_item(mprintf("tkthistory?tn=%d", tn), "History");
  }
  if( g.okWrite ){
    if( rn>0 ){
      common_add_action_item(mprintf("tktedit?tn=%d,%d",tn,rn), "Edit");
    }else{
      common_add_action_item(mprintf("tktedit?tn=%d",tn), "Edit");
    }
    if( attachment_max()>0 ){
      common_add_action_item(mprintf("attach_add?tn=%d",tn), "Attach");
    }
  }
  add_tkt_tools(0,tn);
  common_add_help_item("CvstracTicket");

  /* Check to see how many "extra" ticket fields are defined
  */
  azExtra[0] = db_config("extra1_name",0);
  azExtra[1] = db_config("extra2_name",0);
  azExtra[2] = db_config("extra3_name",0);
  azExtra[3] = db_config("extra4_name",0);
  azExtra[4] = db_config("extra5_name",0);

  /* Get the record out of the database.
  */
  db_add_functions();
  az = db_query("SELECT "
                "  type,"               /* 0 */
                "  status,"             /* 1 */
                "  ldate(origtime),"    /* 2 */
                "  ldate(changetime),"  /* 3 */
                "  derivedfrom,"        /* 4 */
                "  version,"            /* 5 */
                "  assignedto,"         /* 6 */
                "  severity,"           /* 7 */
                "  priority,"           /* 8 */
                "  subsystem,"          /* 9 */
                "  owner,"              /* 10 */
                "  title,"              /* 11 */
                "  description,"        /* 12 */
                "  remarks, "           /* 13 */
                "  contact,"            /* 14 */
                "  extra1,"             /* 15 */
                "  extra2,"             /* 16 */
                "  extra3,"             /* 17 */
                "  extra4,"             /* 18 */
                "  extra5 "             /* 19 */
                "FROM ticket WHERE tn=%d", tn);
  if( az[0]==0 ){
    cgi_redirect("index");
    return;
  }
  azChng = db_query(
    "SELECT chng.date, chng.cn, chng.branch, chng.user, chng.message, chng.milestone "
    "FROM xref, chng WHERE xref.tn=%d AND xref.cn=chng.cn "
    "ORDER BY chng.milestone ASC, chng.date DESC", tn);
  azDrv = db_query(
    "SELECT tn,title FROM ticket WHERE derivedfrom=%d", tn);
  common_header("Ticket #%d", tn);
  cgi_printf("<h2>Ticket %d: %h</h2>\n"
         "<blockquote>\n",tn,az[11]);
  output_formatted(az[12], zPage);
  cgi_printf("</blockquote>\n"
         "\n"
         "<table align=\"right\" hspace=\"10\" cellpadding=2 border=0>\n"
         "<tr><td bgcolor=\"%h\" class=\"border1\">\n"
         "<table width=\"100%%\" border=0 cellpadding=4 cellspacing=0>\n"
         "<tr bgcolor=\"%h\" class=\"bkgnd1\">\n"
         "<td valign=\"top\" align=\"left\">\n",BORDER1,BG1);
  if( az[13][0]==0 ){
    cgi_printf("[<a href=\"tktappend?tn=%h\">Add remarks</a>]\n",zTn);
  } else {
    cgi_printf("[<a href=\"tktappend?tn=%h\">Append remarks</a>]\n",zTn);
  }
  cgi_printf("</td></tr></table></td></tr></table>\n"
         "<h3>Remarks:</h3>\n"
         "<blockquote>\n");
  output_formatted(az[13], zPage);
  cgi_printf("</blockquote>\n");

  if( az[13][0]!=0 ){
    cgi_printf("<table align=\"right\" hspace=\"10\" cellpadding=2 border=0>\n"
           "<tr><td bgcolor=\"%h\" class=\"border1\">\n"
           "<table width=\"100%%\" border=0 cellpadding=4 cellspacing=0>\n"
           "<tr bgcolor=\"%h\" class=\"bkgnd1\">\n"
           "<td valign=\"top\" align=\"left\">\n"
           "[<a href=\"tktappend?tn=%h\">Append remarks</a>]\n"
           "</td></tr></table></td></tr></table>\n"
           "\n",BORDER1,BG1,zTn);
  }

  cgi_printf("\n"
         "<h3>Properties:</h3>\n"
         "\n"
         "<blockquote>\n"
         "<table>\n"
         "<tr>\n"
         "  <td align=\"right\">Type:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
         "  <td align=\"right\">Version:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">Status:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h</b></td>\n"
         "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
         "  <td align=\"right\">Created:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h</b></td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">Severity:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
         "  <td align=\"right\">Last&nbsp;Change:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h</b></td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">Priority:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
         "  <td align=\"right\">Subsystem:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">Assigned&nbsp;To:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n"
         "<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
         "  <td align=\"right\">Derived From:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>\n",BG3,az[0],BG3,az[5],BG3,az[1],BG3,az[2],BG3,az[7],BG3,az[3],BG3,az[8],BG3,az[9],BG3,az[6],BG3);
  z = extract_integer(az[4]);
  if( z && z[0] ){
    z = mprintf("#%s",z);
    output_formatted(z,zPage);
  }else{
    cgi_printf("  &nbsp;\n");
  }
  cgi_printf("  </b></td>\n"
         "</tr>\n"
         "<tr>\n"
         "  <td align=\"right\">Creator:</td>\n"
         "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n",BG3,az[10]);
  if( g.okWrite && !g.isAnon ){
    cgi_printf("<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>\n"
           "  <td align=\"right\">Contact:</td>\n");
    if( strchr(az[14],'@') ){
      cgi_printf("  <td bgcolor=\"%h\" class=\"bkgnd3\"><b><a href=\"mailto:%h\">\n"
             "       %h</a>&nbsp;</b></td>\n",BG3,az[14],az[14]);
    }else{
      cgi_printf("  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n",BG3,az[14]);
    }
    cgi_printf("</tr>\n");
    j = 0;
  } else {
    j = 1;
  }
  for(i=0; i<5; i++){
    if( azExtra[i]==0 ) continue;
    if( j==0 ){
      cgi_printf("<tr>\n");
    }else{
      cgi_printf("<td></td>\n");
    }
    cgi_printf("  <td align=\"right\">%h:</td>\n"
           "  <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h&nbsp;</b></td>\n",azExtra[i],BG3,az[15+i]);
    if( j==0 ){
      j = 1;
    }else{
      cgi_printf("</tr>\n");
      j = 0;
    }
  }
  if( j==1 ){
    cgi_printf("</tr>\n");
  }
  cgi_printf("<tr>\n"
         "</tr>\n"
         "</table>\n"
         "</blockquote>\n");
  if( azDrv[0] ){
    int i;
    cgi_printf("<h3>Derived Tickets:</h3>\n"
           "<table cellspacing=0 border=0 cellpadding=0>\n");
    for(i=0; azDrv[i]; i+=2){
      cgi_printf("<tr><td valign=\"top\" width=160 align=\"right\">\n");
      z = mprintf("#%s",azDrv[i]);
      output_formatted(z,zPage);
      cgi_printf("</td>\n"
             "<td valign=\"center\" width=30 align=\"center\">\n");
      common_icon("ptr1");
      cgi_printf("</td>\n"
             "<td valign=\"top\" align=\"left\">\n");
      output_formatted(azDrv[i+1],0);
      cgi_printf("</td></tr>\n");
    }
    cgi_printf("</table>\n");
  }
  nChng = 0;
  if( azChng[0] && azChng[5] && atoi(azChng[5])==0 ){
    int i;
    cgi_printf("<h3>Related Check-ins:</h3>\n"
           "<table cellspacing=0 border=0 cellpadding=0>\n");
    for(i=0; azChng[i]; i+=6){
      /* Milestones are handeld in loop below */
      if( atoi(azChng[i+5]) ) break;

      nChng++;
      output_tkt_chng(&azChng[i]);
    }
    cgi_printf("</table>\n");
  }
  
  if( azChng[0] && azChng[nChng*6] ){
    int i;
    cgi_printf("<h3>Related Milestones:</h3>\n"
           "<table cellspacing=0 border=0 cellpadding=0>\n");
    for(i=nChng*6; azChng[i]; i+=6){
      output_tkt_chng(&azChng[i]);
    }
    cgi_printf("</table>\n");
  }
  attachment_html(zPage,"<h3>Attachments:</h3>\n<blockquote>","</blockquote>");
  common_footer();
}

/*
** Check to see if the current user is authorized to delete ticket tn.
** Return true if they are and false if not.
**
** Ticket deletion rules:
**
**     * The setup user can delete any ticket at any time.
**
**     * Users other than anonymous with Delete privilege can delete
**       a ticket that was originated by anonymous and has no change
**       by anyone other than anonymous and is less than 24 hours old.
**
**     * Anonymous users can never delete tickets even if they have
**       Delete privilege
*/
static int ok_to_delete_ticket(int tn){
  time_t cutoff = time(0)-86400;
  if( g.okSetup ){
    return 1;
  }
  if( g.isAnon || !g.okDelete ){
    return 0;
  }
  if( db_exists(
     "SELECT 1 FROM ticket"
     " WHERE tn=%d AND (owner!='anonymous' OR origtime<%d)"
     "UNION ALL "
     "SELECT 1 FROM tktchng"
     " WHERE tn=%d AND (user!='anonymous' OR chngtime<%d)",
     tn, cutoff, tn, cutoff)
  ){
    return 0;
  }
  return 1;
}

/*
** WEBPAGE: /tktedit
**
** A webpage for making changes to a ticket
*/
void ticket_edit(void){
  static struct {
    char *zColumn;     /* Name of column in the database */
    char *zName;       /* Name of corresponding query parameter */
    int preserveSpace; /* Preserve initial spaces in text */
    int numeric;       /* Field is a numeric value */
    const char *zOld;  /* Current value of this field */
    const char *zNew;  /* Value of the query parameter */
  } aParm[] = {
    { "type",         "y", 0, 0, },  /* 0 */
    { "status",       "s", 0, 0, },  /* 1 */
    { "derivedfrom",  "d", 0, 1, },  /* 2 */
    { "version",      "v", 0, 0, },  /* 3 */
    { "assignedto",   "a", 0, 0, },  /* 4 */
    { "severity",     "e", 0, 1, },  /* 5 */
    { "priority",     "p", 0, 0, },  /* 6 */
    { "subsystem",    "m", 0, 0, },  /* 7 */
    { "owner",        "w", 0, 0, },  /* 8 */
    { "title",        "t", 0, 0, },  /* 9 */
    { "description",  "c", 1, 0, },  /* 10 */
    { "remarks",      "r", 1, 0, },  /* 11 */
    { "contact",      "n", 0, 0, },  /* 12 */
    { "extra1",      "x1", 0, 0, },  /* 13 */
    { "extra2",      "x2", 0, 0, },  /* 14 */
    { "extra3",      "x3", 0, 0, },  /* 15 */
    { "extra4",      "x4", 0, 0, },  /* 16 */
    { "extra5",      "x5", 0, 0, },  /* 17 */
  };
  int tn = 0;
  int rn = 0;
  int nField;
  int i, j;
  int cnt;
  int isPreview;
  const char *zChngList;
  char *zSep;
  char **az;
  const char **azUsers;
  int *aChng, *aMs;
  int nChng, nMs;
  int nExtra;
  const char *azExtra[5];
  char zPage[30];
  char zSQL[2000];
  char *zErrMsg = 0;

  login_check_credentials();
  if( !g.okWrite ){ login_needed(); return; }
  throttle(1,1);
  isPreview = P("pre")!=0;
  sscanf(PD("tn",""), "%d,%d", &tn, &rn);
  if( tn<=0 ){ cgi_redirect("index"); return; }
  bprintf(zPage,sizeof(zPage),"%d",tn);
  history_update(0);

  if( P("del1") && ok_to_delete_ticket(tn) ){
    char *zTitle = db_short_query("SELECT title FROM ticket "
                                  "WHERE tn=%d", tn);
    if( zTitle==0 ) cgi_redirect("index");

    common_add_action_item(mprintf("tktedit?tn=%h",PD("tn","")), "Cancel");
    common_header("Are You Sure?");
    cgi_printf("<form action=\"tktedit\" method=\"POST\">\n"
           "<p>You are about to delete all traces of ticket\n");
    output_ticket(tn);
    cgi_printf("&nbsp;<strong>%h</strong> from\n"
           "the database.  This is an irreversible operation.  All records\n"
           "related to this ticket will be removed and cannot be recovered.</p>\n"
           "\n"
           "<input type=\"hidden\" name=\"tn\" value=\"%h\">\n"
           "<input type=\"submit\" name=\"del2\" value=\"Delete The Ticket\">\n"
           "<input type=\"submit\" name=\"can\" value=\"Cancel\">\n"
           "</form>\n",zTitle,PD("tn",""));
    common_footer();
    return;
  }
  if( P("del2") && ok_to_delete_ticket(tn) ){
    db_execute(
       "BEGIN;"
       "DELETE FROM ticket WHERE tn=%d;"
       "DELETE FROM tktchng WHERE tn=%d;"
       "DELETE FROM xref WHERE tn=%d;"
       "DELETE FROM attachment WHERE tn=%d;"
       "COMMIT;", tn, tn, tn, tn);
    if( rn>0 ){
      cgi_redirect(mprintf("rptview?rn=%d",rn));
    }else{
      cgi_redirect("index");
    }
    return;
  }

  /* Check to see how many "extra" ticket fields are defined
  */
  nField = sizeof(aParm)/sizeof(aParm[0]);
  azExtra[0] = db_config("extra1_name",0);
  azExtra[1] = db_config("extra2_name",0);
  azExtra[2] = db_config("extra3_name",0);
  azExtra[3] = db_config("extra4_name",0);
  azExtra[4] = db_config("extra5_name",0);
  for(i=nExtra=0; i<5; i++){
    if( azExtra[i]!=0 ){
      nExtra++;
    }else{
      aParm[13+i].zColumn = 0;
    }
  }

  /* Construct a SELECT statement to extract all information we
  ** need from the ticket table.
  */
  j = 0;
  appendf(zSQL,&j,sizeof(zSQL),"SELECT");
  zSep = " ";
  for(i=0; i<nField; i++){
    appendf(zSQL,&j,sizeof(zSQL), "%s%s", zSep,
            aParm[i].zColumn ? aParm[i].zColumn : "''");
    zSep = ",";
  }
  appendf(zSQL,&j,sizeof(zSQL), " FROM ticket WHERE tn=%d", tn);

  /* Execute the SQL.  Load all existing values into aParm[].zOld.
  */
  az = db_query(zSQL);
  if( az==0 || az[0]==0 ){
    cgi_redirect("index");
    return;
  }
  for(i=0; i<nField; i++){
    if( aParm[i].zColumn==0 ) continue;
    aParm[i].zOld = remove_blank_lines(az[i]);
  }

  /* Find out which fields may need to change due to query parameters.
  ** record the new values in aParm[].zNew.
  */
  for(i=cnt=0; i<nField; i++){
    if( aParm[i].zColumn==0 ){ cnt++; continue; }
    aParm[i].zNew = P(aParm[i].zName);
    if( aParm[i].zNew==0 ){
      aParm[i].zNew = aParm[i].zOld;
      if( g.isAnon && aParm[i].zName[0]=='n' ) cnt++;
    }else if( aParm[i].preserveSpace ){
      aParm[i].zNew = remove_blank_lines(aParm[i].zNew);

      /* Only remarks and description fields (i.e. Wiki fields) have
      ** preserve space set. Perfect place to run through edit
      ** heuristics. If it's not allowed, the change won't go through
      ** since the counter won't match.
      */
      zErrMsg = is_edit_allowed(aParm[i].zOld,aParm[i].zNew);
      if( 0==zErrMsg ){
        cnt++;
      }
    }else if( aParm[i].numeric ){
      aParm[i].zNew = extract_integer(aParm[i].zNew);
      cnt++;
    }else{
      aParm[i].zNew = trim_string(aParm[i].zNew);
      cnt++;
    }
  }

  /* The "cl" query parameter holds a list of integer check-in numbers that
  ** this ticket is associated with.  Convert the string into a list of
  ** nChng integers in aChng[].  Or if there is no "cl" query parameter,
  ** extract the list from the database.
  */
  zChngList = P("cl");
  if( zChngList ){
    for(i=nChng=0; zChngList[i]; i++){
      if( isdigit(zChngList[i]) ){
        nChng++;
        while( isdigit(zChngList[i+1]) ) i++;
      }
    }
    aChng = malloc( sizeof(int)*nChng );
    if( aChng==0 ) nChng = 0;
    for(i=j=0; j<nChng && zChngList[i]; i++){
      if( isdigit(zChngList[i]) ){
        aChng[j++] = atoi(&zChngList[i]);
        while( isdigit(zChngList[i+1]) ) i++;
      }
    }
  }else{
    /* FIXME: It would probably be faster to just fetch all XREF.cn and
    ** CHNG.milestone and sort them out manually, so we wouldn't have to run
    ** similar query again for milestones.
    */
    az = db_query(
      "SELECT xref.cn FROM xref, chng WHERE xref.cn=chng.cn AND "
      "chng.milestone=0 AND xref.tn=%d ORDER BY xref.cn", tn
    );
    for(nChng=0; az[nChng]; nChng++){}
    aChng = malloc( sizeof(int)*nChng );
    if( aChng==0 ) nChng = 0;
    for(i=0; i<nChng; i++){
      aChng[i] = atoi(az[i]);
    }
  }
  
  /* Do the same thing here for milestones as we did above for check-ins.
  */
  zChngList = P("ml");
  if( zChngList ){
    for(i=nMs=0; zChngList[i]; i++){
      if( isdigit(zChngList[i]) ){
        nMs++;
        while( isdigit(zChngList[i+1]) ) i++;
      }
    }
    aMs = malloc( sizeof(int)*nMs );
    if( aMs==0 ) nMs = 0;
    for(i=j=0; j<nMs && zChngList[i]; i++){
      if( isdigit(zChngList[i]) ){
        aMs[j++] = atoi(&zChngList[i]);
        while( isdigit(zChngList[i+1]) ) i++;
      }
    }
  }else{
    az = db_query(
      "SELECT xref.cn FROM xref, chng WHERE xref.cn=chng.cn AND "
      "chng.milestone>0 AND xref.tn=%d ORDER BY xref.cn", tn
    );
    for(nMs=0; az[nMs]; nMs++){}
    aMs = malloc( sizeof(int)*nMs );
    if( aMs==0 ) nMs = 0;
    for(i=0; i<nMs; i++){
      aMs[i] = atoi(az[i]);
    }
  }

  /* Update the record in the TICKET table.  Also update the XREF table.
  */
  if( cnt==nField && P("submit")!=0 ){
    time_t now;
    char **az;
    int first_change;
    int last_change;
    
    time(&now);
    db_execute("BEGIN");
    az = db_query(
        "SELECT MAX(ROWID)+1 FROM tktchng"
    );
    first_change = atoi(az[0]);
    for(i=cnt=0; i<nField; i++){
      if( aParm[i].zColumn==0 ) continue;
      if( strcmp(aParm[i].zOld,aParm[i].zNew)==0 ) continue;
      db_execute("UPDATE ticket SET %s='%q' WHERE tn=%d",
         aParm[i].zColumn, aParm[i].zNew, tn);
      db_execute("INSERT INTO tktchng(tn,user,chngtime,fieldid,oldval,newval) "
          "VALUES(%d,'%q',%d,'%s','%q','%q')",
          tn, g.zUser, now, aParm[i].zColumn, aParm[i].zOld, aParm[i].zNew);
      cnt++;
    }
    az = db_query(
        "SELECT MAX(ROWID) FROM tktchng"
        );
    last_change = atoi(az[0]);
    if( cnt ){
      db_execute("UPDATE ticket SET changetime=%d WHERE tn=%d", now, tn);
    }
    db_execute("DELETE FROM xref WHERE tn=%d", tn);
    for(i=0; i<nChng; i++){
      db_execute("INSERT INTO xref(tn,cn) VALUES(%d,%d)", tn, aChng[i]);
    }
    for(i=0; i<nMs; i++){
      /* We don't want to insert same cn twice.
      */
      for(j=0; j<nChng; j++){
        if( aMs[i]==aChng[j] ) break;
      }
      if( j>=nChng ){
        db_execute("INSERT INTO xref(tn,cn) VALUES(%d,%d)", tn, aMs[i]);
      }
    }
    db_execute("COMMIT");
    if( cnt ){
      ticket_notify(tn, first_change, last_change, 0);
    }
    if( rn>0 ){
      cgi_redirect(mprintf("rptview?rn=%d",rn));
    }else{
      cgi_redirect(mprintf("tktview?tn=%d,%d",tn,rn));
    }
    return;
  }

  /* Print the header.
  */
  common_add_action_item( mprintf("tktview?tn=%d,%d", tn, rn), "Cancel");
  if( ok_to_delete_ticket(tn) ){
    common_add_action_item( mprintf("tktedit?tn=%d,%d&del1=1", tn, rn),
                            "Delete");
  }
  common_add_help_item("CvstracTicket");
  common_header("Edit Ticket #%d", tn);

  cgi_printf("<form action=\"tktedit\" method=\"POST\">\n"
         "\n"
         "<input type=\"hidden\" name=\"tn\" value=\"%d,%d\">\n"
         "<nobr>Ticket Number: %d</nobr><br>\n",tn,rn,tn);
  if( zErrMsg ){
    cgi_printf("<blockquote>\n"
           "<font color=\"red\">%h</font>\n"
           "</blockquote>\n",zErrMsg);
  }
  cgi_printf("<nobr>\n"
         "Title: <input type=\"text\" name=\"t\" value=\"%h\"\n"
         "  maxlength=70 size=70>\n"
         "</nobr><br>\n"
         "\n"
         "Description:\n"
         "(<small>See <a href=\"#format_hints\">formatting hints</a></small>)<br>\n"
         "<textarea name=\"c\" rows=\"8\" cols=\"70\" wrap=\"virtual\">\n"
         "%h\n"
         "</textarea><br>\n",aParm[9].zNew,aParm[10].zNew);
  if( isPreview ){
    cgi_printf("<table border=1 cellpadding=15 width=\"100%%\"><tr><td>\n");
    output_formatted(aParm[10].zNew, zPage);
    cgi_printf("&nbsp;</td></tr></table><br>\n");
  }
  cgi_printf("\n"
         "Remarks:\n"
         "(<small>See <a href=\"#format_hints\">formatting hints</a></small>)<br>\n"
         "<textarea name=\"r\" rows=\"8\" cols=\"70\" wrap=\"virtual\">\n"
         "%h\n"
         "</textarea><br>\n",aParm[11].zNew);
  if( isPreview ){
    cgi_printf("<table border=1 cellpadding=15 width=\"100%%\"><tr><td>\n");
    output_formatted(aParm[11].zNew, zPage);
    cgi_printf("&nbsp;</td></tr></table><br>\n");
  }
  cgi_printf("\n"
         "<nobr>\n"
         "Status:\n");
  cgi_v_optionmenu2(0, "s", aParm[1].zNew, (const char**)db_query(
     "SELECT name, value FROM enums WHERE type='status'"));
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Type: \n");
  cgi_v_optionmenu2(0, "y", aParm[0].zNew, (const char**)db_query(
     "SELECT name, value FROM enums WHERE type='type'"));
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "\n"
         "<nobr>\n"
         "Severity: \n");
  cgi_optionmenu(0, "e", aParm[5].zNew,
         "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0);
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Assigned To: \n");
  azUsers = (const char**)db_query(
              "SELECT id FROM user UNION SELECT '' ORDER BY id");
  cgi_v_optionmenu(0, "a", aParm[4].zNew, azUsers);
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Subsystem:\n");
  cgi_v_optionmenu2(0, "m", aParm[7].zNew, (const char**)db_query(
      "SELECT '','' UNION ALL "
      "SELECT name, value FROM enums WHERE type='subsys'"));
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Version: <input type=\"text\" name=\"v\" value=\"%h\" size=10>\n"
         "</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Derived From: <input type=\"text\" name=\"d\" value=\"%h\" size=10>\n"
         "</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Priority:\n",aParm[3].zNew,aParm[2].zNew);
  cgi_optionmenu(0, "p", aParm[6].zNew,
         "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0);
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<nobr>\n"
         "Owner: \n");
  cgi_v_optionmenu(0, "w", aParm[8].zNew, azUsers);
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n");
  if( !g.isAnon ){
    cgi_printf("<nobr>\n"
           "Contact: <input type=\"text\" name=\"n\" value=\"%h\" size=20>\n"
           "</nobr>\n"
           "&nbsp;&nbsp;&nbsp;\n"
           "\n",aParm[12].zNew);
  }
  for(i=0; i<5; i++){
    char **az;
    char zX[3];

    if( azExtra[i]==0 ) continue;
    az = db_query("SELECT name, value FROM enums "
                   "WHERE type='extra%d'", i+1);
    bprintf(zX, sizeof(zX), "x%d", i+1);
    cgi_printf("<nobr>\n"
           "%h:\n",azExtra[i]);
    if( az && az[0] ){
      cgi_v_optionmenu2(0, zX, aParm[13+i].zNew, (const char **)az);
    }else{
      cgi_printf("<input type=\"text\" name=\"%h\" value=\"%h\" size=20>\n",zX,aParm[13+i].zNew);
    }
    db_query_free(az);
    cgi_printf("</nobr>\n"
           "&nbsp;&nbsp;&nbsp;\n"
           "\n");
  }
  cgi_printf("<nobr>\n"
         "Associated Check-ins:\n");
  cgi_printf("<input type=\"text\" name=\"cl\" size=70 value=\"");
  zSep = "";
  for(i=0; i<nChng; i++){
    cgi_printf("%s%d", zSep, aChng[i]);
    zSep = " ";
  }
  cgi_printf("\">\n");
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "<nobr>\n"
         "Associated Milestones:\n");
  cgi_printf("<input type=\"text\" name=\"ml\" size=70 value=\"");
  zSep = "";
  for(i=0; i<nMs; i++){
    cgi_printf("%s%d", zSep, aMs[i]);
    zSep = " ";
  }
  cgi_printf("\">\n");
  cgi_printf("</nobr>\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "\n"
         "<p align=\"center\">\n"
         "<input type=\"submit\" name=\"submit\" value=\"Apply Changes\">\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "<input type=\"submit\" name=\"pre\" value=\"Preview Description And Remarks\">\n");
  if( ok_to_delete_ticket(tn) ){
    cgi_printf("&nbsp;&nbsp;&nbsp;\n"
           "<input type=\"submit\" name=\"del1\" value=\"Delete This Ticket\">\n");
  }
  cgi_printf("</p>\n"
         "\n"
         "</form>\n");
  attachment_html(mprintf("%d",tn),"<h3>Attachments</h3><blockquote>",
      "</blockquote>");
  cgi_printf("\n"
         "<a name=\"format_hints\">\n"
         "<hr>\n"
         "<h3>Formatting Hints:</h3>\n");
  append_formatting_hints();
  common_footer();
}

/*
** WEBPAGE: /tktappend
**
** Append remarks to a ticket
*/
void ticket_append(void){
  int tn, rn;
  char zPage[30];
  int doPreview;
  int doSubmit;
  const char *zText;
  const char *zTn;
  char *zErrMsg = 0;
  char *zTktTitle;

  login_check_credentials();
  if( !g.okWrite ){ login_needed(); return; }
  throttle(1,1);
  tn = rn = 0;
  zTn = PD("tn","");
  sscanf(zTn, "%d,%d", &tn, &rn);
  if( tn<=0 ){ cgi_redirect("index"); return; }
  bprintf(zPage,sizeof(zPage),"%d",tn);
  doPreview = P("pre")!=0;
  doSubmit = P("submit")!=0;
  zText = remove_blank_lines(PD("r",""));
  if( doSubmit ){
    zErrMsg = is_edit_allowed(0,zText);
    if( zText[0] && 0==zErrMsg ){
      time_t now;
      struct tm *pTm;
      char zDate[200];
      const char *zOrig;
      char *zNew;
      char *zSpacer = " {linebreak}\n";
      char *zHLine = "\n\n----\n";
      char **az;
      int change;
      zOrig = db_short_query("SELECT remarks FROM ticket WHERE tn=%d", tn);
      zOrig = remove_blank_lines(zOrig);
      time(&now); 
      pTm = localtime(&now);
      strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
      if( isspace(zText[0]) && isspace(zText[1]) ) zSpacer = "\n\n";
      if( zOrig[0]==0 ) zHLine = "";
      zNew = mprintf("%s_%s by %s:_%s%s",
                     zHLine, zDate, g.zUser, zSpacer, zText);
      db_execute(
        "BEGIN;"
        "UPDATE ticket SET remarks='%q%q', changetime=%d WHERE tn=%d;"
        "INSERT INTO tktchng(tn,user,chngtime,fieldid,oldval,newval) "
           "VALUES(%d,'%q',%d,'remarks','%q','%q%q');"
        "COMMIT;",
        zOrig, zNew, now, tn,
        tn, g.zUser, now, zOrig, zOrig, zNew
      );
      az = db_query(
          "SELECT MAX(ROWID) FROM tktchng"
          );
      change = atoi(az[0]);
      ticket_notify(tn, change, change, 0);
      cgi_redirect(mprintf("tktview?tn=%h",zTn));
    }
  }
  zTktTitle = db_short_query("SELECT title FROM ticket WHERE tn=%d", tn);
  
  common_add_help_item("CvstracTicket");
  common_add_action_item( mprintf("tktview?tn=%h", zTn), "Cancel");
  common_header("Append Remarks To Ticket #%d", tn);

  if( zErrMsg ){
    cgi_printf("<blockquote>\n"
           "<font color=\"red\">%h</font>\n"
           "</blockquote>\n",zErrMsg);
  }

  cgi_printf("<form action=\"tktappend\" method=\"POST\">\n"
         "<input type=\"hidden\" name=\"tn\" value=\"%h\">\n"
         "Append to #%d:\n",zTn,tn);
  cgi_href(zTktTitle, 0, 0, 0, 0, 0, "tktview?tn=%d", tn);
  cgi_printf("&nbsp;\n"
         "(<small>See <a href=\"#format_hints\">formatting hints</a></small>)<br>\n"
         "<textarea name=\"r\" rows=\"8\" cols=\"70\" wrap=\"virtual\">%h</textarea>\n"
         "<br>\n"
         "<p align=\"center\">\n"
         "<input type=\"submit\" name=\"submit\" value=\"Apply\">\n"
         "&nbsp;&nbsp;&nbsp;\n"
         "<input type=\"submit\" name=\"pre\" value=\"Preview\">\n"
         "</p>\n",zText);
  if( doPreview ){
    cgi_printf("<table border=1 cellpadding=15 width=\"100%%\"><tr><td>\n");
    output_formatted(zText, zPage);
    cgi_printf("&nbsp;</td></tr></table><br>\n");
  }
  cgi_printf("\n"
         "</form>\n"
         "<a name=\"format_hints\">\n"
         "<hr>\n"
         "<h3>Formatting Hints:</h3>\n");
  append_formatting_hints();
  common_footer();
}

/*
** Output a ticket change record. isLast indicates it's the last
** ticket change and _might_ be subject to undo.
*/
static void ticket_change(
  time_t date,        /* date/time of the change */
  int tn,             /* ticket number */
  const char *zUser,  /* user that made the change */
  const char *zField, /* field that changed */
  const char *zOld,   /* old value */
  const char *zNew,   /* new value */
  int isLast          /* non-zero if last ticket change in the history */
){
  struct tm *pTm;
  char zDate[100];
  char zPage[30];

  bprintf(zPage,sizeof(zPage),"%d",tn);

  pTm = localtime(&date);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm);

  cgi_printf("<li>\n");

  if( strcmp(zField,"description")==0 || strcmp(zField,"remarks")==0 ){
    int len1, len2;
    len1 = strlen(zOld);
    len2 = strlen(zNew);
    if( len1==0 ){
      cgi_printf("Added <i>%h</i>:<blockquote>\n",zField);
      output_formatted(&zNew[len1], zPage);
      cgi_printf("</blockquote>\n");
    }else if( len2>len1+5 && strncmp(zOld,zNew,len1)==0 ){
      cgi_printf("Appended to <i>%h</i>:<blockquote>\n",zField);
      output_formatted(&zNew[len1], zPage);
      cgi_printf("</blockquote>\n");
    }else{
      cgi_printf("Changed <i>%h</i>.\n",zField);
      diff_strings(1,zOld,zNew);
    }
  }else if( (!g.okWrite || g.isAnon) && strcmp(zField,"contact")==0 ){
    /* Do not show contact information to unprivileged users */
    cgi_printf("Change <i>%h</i>\n",zField);
  }else if( strncmp(zField,"extra",5)==0 ){
    char zLabel[30];
    const char *zAlias;
    bprintf(zLabel,sizeof(zLabel),"%h_name", zField);
    zAlias = db_config(zLabel, zField);
    cgi_printf("Change <i>%h</i> from \"%h\" to \"%h\"\n",zAlias,zOld,zNew);
  }else{
    cgi_printf("Change <i>%h</i> from \"%h\" to \"%h\"\n",zField,zOld,zNew);
  }

  cgi_printf("by %h on %h\n",zUser,zDate);

  if( isLast && ok_to_undo_change(date, zUser) ){
    cgi_printf("[<a href=\"tktundo?tn=%d&u=%t&t=%d\">Undo\n"
           "this change</a>]</p>\n",tn,zUser,date);
  }

  cgi_printf("</li>\n");
}

/*
** Output a checkin record.
*/
static void ticket_checkin(
  time_t date,          /* date/time of the change */
  int cn,               /* change number */
  const char *zBranch,  /* branch of the change, may be NULL */
  const char *zUser,    /* user name that made the change */
  const char *zMessage  /* log message for the change */
){
  struct tm *pTm;
  char *z;
  char zDate[100];

  cgi_printf("<li> Check-in \n");

  output_chng(cn);
  if( zBranch && zBranch[0] ){
    cgi_printf("on branch %h:\n",zBranch);
  } else {
    cgi_printf(": "); /* want the : right up against the [cn] */
  }

  z = strdup(zMessage);
  if( output_trim_message(z, MN_CKIN_MSG, MX_CKIN_MSG) ){
    output_formatted(z, 0);
    cgi_printf("&nbsp;[...]\n");
  }else{
    output_formatted(z, 0);
  }

  pTm = localtime(&date);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm);
  cgi_printf("(By %h on %h)</li>\n"
         "</li>\n",zUser,zDate);
}

/*
** Output an attachment record.
*/
static void ticket_attach(
  time_t date,          /* date/time of the attachment */
  int attachn,          /* attachment number */
  size_t size,          /* size, in bytes, of the attachment */
  const char *zUser,    /* username that created it */
  const char *zDescription,    /* description of the attachment */
  const char *zFilename /* name of attachment file */
){
  char zDate[100];
  struct tm *pTm;
  pTm = localtime(&date);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
  cgi_printf("<li> Attachment \n"
         "<a href=\"attach_get/%d/%h\">%h</a>\n"
         "%d bytes added by %h on %h.\n",attachn,zFilename,zFilename,size,zUser,zDate);
  if( zDescription && zDescription[0] ){
    cgi_printf("<br>\n");
    output_formatted(zDescription,NULL);
    cgi_printf("<br>\n");
  }
  if( ok_to_delete_attachment(date, zUser) ){
    cgi_printf("[<a href=\"attach_del?atn=%d\">delete</a>]\n",attachn);
  }
  cgi_printf("</li>\n");
}

/*
** Output an inspection note.
*/
static void ticket_inspect(
  time_t date,              /* date/time of the inspection */
  int cn,                   /* change that was inspected */
  const char *zInspector,   /* username that did the inspection */
  const char *zResult       /* string describing the result */
){
  char zDate[100];
  struct tm *pTm;
  pTm = localtime(&date);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
  cgi_printf("<li> Inspection report \"%h\" on \n",zResult);
  output_chng(cn);
  cgi_printf("&nbsp;by %h on %h\n"
         "</li>\n",zInspector,zDate);
}

/*
** Output a derived ticket creation
*/
static void ticket_derived(
  time_t date,        /* date/time derived ticket was created */
  int tn,             /* number of derived ticket */
  const char* zOwner, /* creator of derived ticket */
  const char *zTitle  /* (currently unused) title of derived ticket */
){
  char zDate[100];
  struct tm *pTm;
  pTm = localtime(&date);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
  cgi_printf("<li> Derived \n");
  output_ticket(tn);
  cgi_printf("&nbsp;by %h on %h\n"
         "</li>\n",zOwner,zDate);
}

/*
** WEBPAGE: /tkthistory
**
** A webpage for viewing the history of a ticket. The history is a
** chronological mix of ticket actions, checkins, attachments, etc.
*/
void ticket_history(void){
  int tn = 0, rn = 0;
  int lasttn = 0;
  char **az;
  int i;
  char zPage[30];
  const char *zTn;
  time_t orig;
  char zDate[200];
  struct tm *pTm;

  login_check_credentials();
  if( !g.okRead ){ login_needed(); return; }
  throttle(1,0);
  history_update(0);
  zTn = PD("tn","");
  sscanf(zTn, "%d,%d", &tn, &rn);
  if( tn<=0 ){ cgi_redirect("index"); return; }

  bprintf(zPage,sizeof(zPage),"%d",tn);
  common_standard_menu("tktview", "search?t=1");

  if( rn>0 ){
    common_add_action_item(mprintf("tktview?tn=%d,%d",tn,rn), "View");
  }else{
    common_add_action_item(mprintf("tktview?tn=%d",tn), "View");
  }

  common_add_help_item("CvstracTicket");

  if( g.okWrite ){
    if( rn>0 ){
      common_add_action_item(mprintf("tktedit?tn=%d,%d",tn,rn), "Edit");
    }else{
      common_add_action_item(mprintf("tktedit?tn=%d",tn), "Edit");
    }
    if( attachment_max()>0 ){
      common_add_action_item(mprintf("attach_add?tn=%d",tn), "Attach");
    }
  }
  add_tkt_tools(0,tn);

  /* Get the record from the database.
  */
  db_add_functions();
  az = db_query("SELECT title,origtime,owner FROM ticket WHERE tn=%d", tn);
  if( az == NULL || az[0]==0 ){
    cgi_redirect("index");
    return;
  }

  orig = atoi(az[1]);
  pTm = localtime(&orig);
  strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);

  common_header("Ticket #%d History", tn);
  cgi_printf("<h2>Ticket %d History: %h</h2>\n"
         "<ol>\n"
         "<li>Created %h by %h</li>\n",tn,az[0],zDate,az[2]);

  /* Grab various types of ticket activities from the db.
  ** All must be sorted by ascending time and the first field of each
  ** record should be epoch time. Second field is the record type.
  */
  az = db_query(
    /* Ticket changes
    */
    "SELECT chngtime AS 'time', 1 AS 'type', "
      "user, fieldid, oldval, newval, NULL "
    "FROM tktchng WHERE tn=%d "
    "UNION ALL "

    /* Checkins
    */
    "SELECT chng.date AS 'time', 2 AS 'type', "
       " chng.cn, chng.branch, chng.user, chng.message, chng.milestone "
    "FROM xref, chng WHERE xref.tn=%d AND xref.cn=chng.cn "
    "UNION ALL "

    /* attachments
    */
    "SELECT date AS 'time', 3 AS 'type', atn, size, user, description, fname "
    "FROM attachment WHERE tn=%d "
    "UNION ALL "

    /* inspection reports
    */
    "SELECT inspect.inspecttime AS 'time', 4 AS 'type', "
      "inspect.cn, inspect.inspector, inspect.result, NULL, NULL "
    "FROM xref, inspect "
    "WHERE xref.cn=inspect.cn AND xref.tn=%d "
    "UNION ALL "

    /* derived tickets. This is just the derived ticket creation. Could
    ** also report derived ticket changes, but we'd probably have to
    ** use some kind of tree representation.
    */
    "SELECT origtime AS 'time', 5 AS 'type', tn, owner, title, NULL, NULL "
    "FROM ticket WHERE derivedfrom=%d "

    "ORDER BY 1, 2",
    tn, tn, tn, tn, tn);

  /* find the last ticket change in the list. This is necessary to allow
  ** someone to undo the last change.
  */
  for(i=0; az[i]; i+=7){
    int type = atoi(az[i+1]);
    if( type==1 ) lasttn = i;
  }

  for(i=0; az[i]; i+=7) {
    time_t date = atoi(az[i]);
    int type = atoi(az[i+1]);
    switch( type ){
      case 1: { /* ticket change */
        ticket_change(date, tn, az[i+2],
          az[i+3], az[i+4], az[i+5], lasttn==i);
        break;
      }
      case 2: { /* checkin */
        ticket_checkin(date, atoi(az[i+2]), az[i+3], az[i+4], az[i+5]);
        break;
      }
      case 3: { /* attachment */
        ticket_attach(date, atoi(az[i+2]), atoi(az[i+3]),
          az[i+4], az[i+5], az[i+6]);
        break;
      }
      case 4: { /* inspection report */
        ticket_inspect(date, atoi(az[i+2]), az[i+3], az[i+4]);
        break;
      }
      case 5: { /* derived ticket creation */
        ticket_derived(date, atoi(az[i+2]), az[i+3], az[i+4]);
        break;
      }
      default:
        /* Can't happen */
        /* assert( type >= 1 && type <= 5 ); */
        break;
    }
  }
  cgi_printf("</ol>\n");
  common_footer();
}


syntax highlighted by Code2HTML, v. 0.9.1