/* ** 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 ){ cgi_printf("
\n" "%h\n" "
\n",zErrMsg); } cgi_printf("
\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" "\n" " \n" "\n" "\n" " \n" " \n" "\n" "\n" "\n" " \n" " \n" "\n" "\n" "\n" " \n" " \n" "\n" "\n"); if( g.okWrite ){ cgi_printf("\n" " \n" " \n" "\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("\n" " \n" " \n" "\n"); } } cgi_printf("\n" " \n" " \n" "\n",zFrom); if( g.isAnon ){ cgi_printf("\n" " \n" " \n" "\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("\n" " \n" " \n" "\n" "\n",zDesc); } cgi_printf("\n" " \n" "\n" "\n" " \n" " \n" "\n" "\n" " \n" " \n" "\n" "
\n" "Enter a one-line summary of the problem:
\n" "\n" "
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("What type of ticket is this?
\n" " Version: \n" " \n" " Enter the version and/or build number of the product\n" " that exhibits the problem.\n" "
\n" " Severity:\n",zVers); cgi_optionmenu(0, "r", zSev, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); cgi_printf(" \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" "
\n" " Priority:\n"); cgi_optionmenu(0, "p", zPri, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); cgi_printf(" \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" "
\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(" \n" " To what user should this problem be assigned?\n" "
\n" " Subsystem:\n"); cgi_v_optionmenu2(4, "s", zSubsys, (const char**)az); db_query_free(az); cgi_printf(" \n" " Which component is showing a problem?\n" "
\n" " Derived From: \n" " \n" " Is this related to an existing ticket?\n" "
\n" " Contact: \n" " \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" "
\n" " %h:\n",zName); if( az==0 || az[0]==0 ){ cgi_printf(" \n",zX,PD(zX,"")); }else{ cgi_v_optionmenu2(0, zX, PD(zX,az[0]), (const char**)az); } cgi_printf(" \n"); /* description is already HTML markup */ cgi_printf(" %s\n" "
\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" " Formatting hints.\n" "
\n" "\n",zDesc); if( isPreview ){ cgi_printf("
Description Preview:\n" "
\n"); output_formatted(zDesc, 0); cgi_printf("
\n"); } if( g.okWrite ){ cgi_printf("
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("
\n" " \n" " \n" " Preview the formatting of the description.\n" "
\n" " \n" " \n" " After filling in the information about, press this button to create\n" " the new ticket.\n" "
\n" "
\n" "\n" "
\n" "

Formatting Hints:

\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 || chngTimeIf you really want to remove the last edit to ticket #%d\n" "then click on the \"OK\" link below. Otherwise, click on \"Cancel\".

\n" "
\n" "\n" "\n" "\n" "\n" "\n" "
\n" "\n" "\n" "\n" "
\n" "
\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("%h\n" "\n",zDate); common_icon("dot"); cgi_printf("\n" " \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("\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("

Ticket %d: %h

\n" "
\n",tn,az[11]); output_formatted(az[12], zPage); cgi_printf("
\n" "\n" "\n" "
\n" "\n" "\n" "
\n",BORDER1,BG1); if( az[13][0]==0 ){ cgi_printf("[Add remarks]\n",zTn); } else { cgi_printf("[Append remarks]\n",zTn); } cgi_printf("
\n" "

Remarks:

\n" "
\n"); output_formatted(az[13], zPage); cgi_printf("
\n"); if( az[13][0]!=0 ){ cgi_printf("\n" "
\n" "\n" "\n" "
\n" "[Append remarks]\n" "
\n" "\n",BORDER1,BG1,zTn); } cgi_printf("\n" "

Properties:

\n" "\n" "
\n" "\n" "\n" " \n" " \n" "\n" " \n" " \n" "\n" "\n" " \n" " \n" "\n" " \n" " \n" "\n" "\n" " \n" " \n" "\n" " \n" " \n" "\n" "\n" " \n" " \n" "\n" " \n" " \n" "\n" "\n" " \n" " \n" "\n" " \n" " \n" "\n" "\n" " \n" " \n",BG3,az[10]); if( g.okWrite && !g.isAnon ){ cgi_printf("\n" " \n"); if( strchr(az[14],'@') ){ cgi_printf(" \n",BG3,az[14],az[14]); }else{ cgi_printf(" \n",BG3,az[14]); } cgi_printf("\n"); j = 0; } else { j = 1; } for(i=0; i<5; i++){ if( azExtra[i]==0 ) continue; if( j==0 ){ cgi_printf("\n"); }else{ cgi_printf("\n"); } cgi_printf(" \n" " \n",azExtra[i],BG3,az[15+i]); if( j==0 ){ j = 1; }else{ cgi_printf("\n"); j = 0; } } if( j==1 ){ cgi_printf("\n"); } cgi_printf("\n" "\n" "
Type:%h         Version:%h 
Status:%h        Created:%h
Severity:%h         Last Change:%h
Priority:%h         Subsystem:%h 
Assigned To:%h         Derived From:\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("
Creator:%h         Contact:\n" " %h %h 
%h:%h 
\n" "
\n"); if( azDrv[0] ){ int i; cgi_printf("

Derived Tickets:

\n" "\n"); for(i=0; azDrv[i]; i+=2){ cgi_printf("\n" "\n" "\n"); } cgi_printf("
\n"); z = mprintf("#%s",azDrv[i]); output_formatted(z,zPage); cgi_printf("\n"); common_icon("ptr1"); cgi_printf("\n"); output_formatted(azDrv[i+1],0); cgi_printf("
\n"); } nChng = 0; if( azChng[0] && azChng[5] && atoi(azChng[5])==0 ){ int i; cgi_printf("

Related Check-ins:

\n" "\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("
\n"); } if( azChng[0] && azChng[nChng*6] ){ int i; cgi_printf("

Related Milestones:

\n" "\n"); for(i=nChng*6; azChng[i]; i+=6){ output_tkt_chng(&azChng[i]); } cgi_printf("
\n"); } attachment_html(zPage,"

Attachments:

\n
","
"); 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("
\n" "

You are about to delete all traces of ticket\n"); output_ticket(tn); cgi_printf(" %h from\n" "the database. This is an irreversible operation. All records\n" "related to this ticket will be removed and cannot be recovered.

\n" "\n" "\n" "\n" "\n" "
\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; i0 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=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("
\n" "\n" "\n" "Ticket Number: %d
\n",tn,rn,tn); if( zErrMsg ){ cgi_printf("
\n" "%h\n" "
\n",zErrMsg); } cgi_printf("\n" "Title: \n" "
\n" "\n" "Description:\n" "(See
formatting hints)
\n" "
\n",aParm[9].zNew,aParm[10].zNew); if( isPreview ){ cgi_printf("
\n"); output_formatted(aParm[10].zNew, zPage); cgi_printf(" 

\n"); } cgi_printf("\n" "Remarks:\n" "(See formatting hints)
\n" "
\n",aParm[11].zNew); if( isPreview ){ cgi_printf("
\n"); output_formatted(aParm[11].zNew, zPage); cgi_printf(" 

\n"); } cgi_printf("\n" "\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("\n" "   \n" "\n" "\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("\n" "   \n" "\n" "\n" "\n" "Severity: \n"); cgi_optionmenu(0, "e", aParm[5].zNew, "1", "1", "2", "2", "3", "3", "4", "4", "5", "5", 0); cgi_printf("\n" "   \n" "\n" "\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("\n" "   \n" "\n" "\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("\n" "   \n" "\n" "\n" "Version: \n" "\n" "   \n" "\n" "\n" "Derived From: \n" "\n" "   \n" "\n" "\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("\n" "   \n" "\n" "\n" "Owner: \n"); cgi_v_optionmenu(0, "w", aParm[8].zNew, azUsers); cgi_printf("\n" "   \n" "\n"); if( !g.isAnon ){ cgi_printf("\n" "Contact: \n" "\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("\n" "%h:\n",azExtra[i]); if( az && az[0] ){ cgi_v_optionmenu2(0, zX, aParm[13+i].zNew, (const char **)az); }else{ cgi_printf("\n",zX,aParm[13+i].zNew); } db_query_free(az); cgi_printf("\n" "   \n" "\n"); } cgi_printf("\n" "Associated Check-ins:\n"); cgi_printf("\n"); cgi_printf("\n" "   \n" "\n" "Associated Milestones:\n"); cgi_printf("\n"); cgi_printf("\n" "   \n" "\n" "

\n" "\n" "   \n" "\n"); if( ok_to_delete_ticket(tn) ){ cgi_printf("   \n" "\n"); } cgi_printf("

\n" "\n" "
\n"); attachment_html(mprintf("%d",tn),"

Attachments

", "
"); cgi_printf("\n" "\n" "
\n" "

Formatting Hints:

\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("
\n" "%h\n" "
\n",zErrMsg); } cgi_printf("
\n" "\n" "Append to #%d:\n",zTn,tn); cgi_href(zTktTitle, 0, 0, 0, 0, 0, "tktview?tn=%d", tn); cgi_printf(" \n" "(See formatting hints)
\n" "\n" "
\n" "

\n" "\n" "   \n" "\n" "

\n",zText); if( doPreview ){ cgi_printf("
\n"); output_formatted(zText, zPage); cgi_printf(" 

\n"); } cgi_printf("\n" "
\n" "\n" "
\n" "

Formatting Hints:

\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("
  • \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 %h:
    \n",zField); output_formatted(&zNew[len1], zPage); cgi_printf("
    \n"); }else if( len2>len1+5 && strncmp(zOld,zNew,len1)==0 ){ cgi_printf("Appended to %h:
    \n",zField); output_formatted(&zNew[len1], zPage); cgi_printf("
    \n"); }else{ cgi_printf("Changed %h.\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 %h\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 %h from \"%h\" to \"%h\"\n",zAlias,zOld,zNew); }else{ cgi_printf("Change %h 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("[
    Undo\n" "this change]

    \n",tn,zUser,date); } cgi_printf("
  • \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("
  • 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)
  • \n" "\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("
  • Attachment \n" "%h\n" "%d bytes added by %h on %h.\n",attachn,zFilename,zFilename,size,zUser,zDate); if( zDescription && zDescription[0] ){ cgi_printf("
    \n"); output_formatted(zDescription,NULL); cgi_printf("
    \n"); } if( ok_to_delete_attachment(date, zUser) ){ cgi_printf("[delete]\n",attachn); } cgi_printf("
  • \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("
  • Inspection report \"%h\" on \n",zResult); output_chng(cn); cgi_printf(" by %h on %h\n" "
  • \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("
  • Derived \n"); output_ticket(tn); cgi_printf(" by %h on %h\n" "
  • \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("

    Ticket %d History: %h

    \n" "
      \n" "
    1. Created %h by %h
    2. \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("
    \n"); common_footer(); }