/*
** 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
/*
** 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; i0 ){
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; i70 ){
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 ){
@
@
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]);
@
}
/*
** 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];
@
Check-in
output_chng(cn);
if( zBranch && zBranch[0] ){
@ on branch %h(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);
@ [...]
}else{
output_formatted(z, 0);
}
pTm = localtime(&date);
strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M", pTm);
@ (By %h(zUser) on %h(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);
@
}
/*
** 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);
@
Inspection report "%h(zResult)" on
output_chng(cn);
@ by %h(zInspector) on %h(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);
@
Derived
output_ticket(tn);
@ by %h(zOwner) on %h(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);
@
Ticket %d(tn) History: %h(az[0])
@
@
Created %h(zDate) by %h(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;
}
}
@
common_footer();
}