/*
** 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(" [...]\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 </b></td>\n"
"<td> </td>\n"
" <td align=\"right\">Version:</td>\n"
" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h </b></td>\n"
"</tr>\n"
"<tr>\n"
" <td align=\"right\">Status:</td>\n"
" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h</b></td>\n"
"<td> </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 </b></td>\n"
"<td> </td>\n"
" <td align=\"right\">Last 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 </b></td>\n"
"<td> </td>\n"
" <td align=\"right\">Subsystem:</td>\n"
" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h </b></td>\n"
"</tr>\n"
"<tr>\n"
" <td align=\"right\">Assigned To:</td>\n"
" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h </b></td>\n"
"<td> </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(" \n");
}
cgi_printf(" </b></td>\n"
"</tr>\n"
"<tr>\n"
" <td align=\"right\">Creator:</td>\n"
" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h </b></td>\n",BG3,az[10]);
if( g.okWrite && !g.isAnon ){
cgi_printf("<td> </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> </b></td>\n",BG3,az[14],az[14]);
}else{
cgi_printf(" <td bgcolor=\"%h\" class=\"bkgnd3\"><b>%h </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 </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(" <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(" </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(" </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"
" \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"
" \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"
" \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"
" \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"
" \n"
"\n"
"<nobr>\n"
"Version: <input type=\"text\" name=\"v\" value=\"%h\" size=10>\n"
"</nobr>\n"
" \n"
"\n"
"<nobr>\n"
"Derived From: <input type=\"text\" name=\"d\" value=\"%h\" size=10>\n"
"</nobr>\n"
" \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"
" \n"
"\n"
"<nobr>\n"
"Owner: \n");
cgi_v_optionmenu(0, "w", aParm[8].zNew, azUsers);
cgi_printf("</nobr>\n"
" \n"
"\n");
if( !g.isAnon ){
cgi_printf("<nobr>\n"
"Contact: <input type=\"text\" name=\"n\" value=\"%h\" size=20>\n"
"</nobr>\n"
" \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"
" \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"
" \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"
" \n"
"\n"
"<p align=\"center\">\n"
"<input type=\"submit\" name=\"submit\" value=\"Apply Changes\">\n"
" \n"
"<input type=\"submit\" name=\"pre\" value=\"Preview Description And Remarks\">\n");
if( ok_to_delete_ticket(tn) ){
cgi_printf(" \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(" \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"
" \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(" </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(" [...]\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(" 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(" 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