1 /*
2 ** Copyright (c) 2007 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code used render and control ticket entry
19 ** and display pages.
20 */
21 #include "config.h"
22 #include "tkt.h"
23 #include <assert.h>
24 
25 /*
26 ** The list of database user-defined fields in the TICKET table.
27 ** The real table also contains some addition fields for internal
28 ** used.  The internal-use fields begin with "tkt_".
29 */
30 static int nField = 0;
31 static struct tktFieldInfo {
32   char *zName;             /* Name of the database field */
33   char *zValue;            /* Value to store */
34   char *zAppend;           /* Value to append */
35   unsigned mUsed;          /* 01: TICKET  02: TICKETCHNG */
36 } *aField;
37 #define USEDBY_TICKET      01
38 #define USEDBY_TICKETCHNG  02
39 #define USEDBY_BOTH        03
40 static u8 haveTicket = 0;        /* True if the TICKET table exists */
41 static u8 haveTicketCTime = 0;   /* True if TICKET.TKT_CTIME exists */
42 static u8 haveTicketChng = 0;    /* True if the TICKETCHNG table exists */
43 static u8 haveTicketChngRid = 0; /* True if TICKETCHNG.TKT_RID exists */
44 
45 /*
46 ** Compare two entries in aField[] for sorting purposes
47 */
nameCmpr(const void * a,const void * b)48 static int nameCmpr(const void *a, const void *b){
49   return fossil_strcmp(((const struct tktFieldInfo*)a)->zName,
50                        ((const struct tktFieldInfo*)b)->zName);
51 }
52 
53 /*
54 ** Return the index into aField[] of the given field name.
55 ** Return -1 if zFieldName is not in aField[].
56 */
fieldId(const char * zFieldName)57 static int fieldId(const char *zFieldName){
58   int i;
59   for(i=0; i<nField; i++){
60     if( fossil_strcmp(aField[i].zName, zFieldName)==0 ) return i;
61   }
62   return -1;
63 }
64 
65 /*
66 ** Obtain a list of all fields of the TICKET and TICKETCHNG tables.  Put them
67 ** in sorted order in aField[].
68 **
69 ** The haveTicket and haveTicketChng variables are set to 1 if the TICKET and
70 ** TICKETCHANGE tables exist, respectively.
71 */
getAllTicketFields(void)72 static void getAllTicketFields(void){
73   Stmt q;
74   int i;
75   static int once = 0;
76   if( once ) return;
77   once = 1;
78   db_prepare(&q, "PRAGMA table_info(ticket)");
79   while( db_step(&q)==SQLITE_ROW ){
80     const char *zFieldName = db_column_text(&q, 1);
81     haveTicket = 1;
82     if( memcmp(zFieldName,"tkt_",4)==0 ){
83       if( strcmp(zFieldName, "tkt_ctime")==0 ) haveTicketCTime = 1;
84       continue;
85     }
86     if( nField%10==0 ){
87       aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
88     }
89     aField[nField].zName = mprintf("%s", zFieldName);
90     aField[nField].mUsed = USEDBY_TICKET;
91     nField++;
92   }
93   db_finalize(&q);
94   db_prepare(&q, "PRAGMA table_info(ticketchng)");
95   while( db_step(&q)==SQLITE_ROW ){
96     const char *zFieldName = db_column_text(&q, 1);
97     haveTicketChng = 1;
98     if( memcmp(zFieldName,"tkt_",4)==0 ){
99       if( strcmp(zFieldName,"tkt_rid")==0 ) haveTicketChngRid = 1;
100       continue;
101     }
102     if( (i = fieldId(zFieldName))>=0 ){
103       aField[i].mUsed |= USEDBY_TICKETCHNG;
104       continue;
105     }
106     if( nField%10==0 ){
107       aField = fossil_realloc(aField, sizeof(aField[0])*(nField+10) );
108     }
109     aField[nField].zName = mprintf("%s", zFieldName);
110     aField[nField].mUsed = USEDBY_TICKETCHNG;
111     nField++;
112   }
113   db_finalize(&q);
114   qsort(aField, nField, sizeof(aField[0]), nameCmpr);
115   for(i=0; i<nField; i++){
116     aField[i].zValue = "";
117     aField[i].zAppend = 0;
118   }
119 }
120 
121 /*
122 ** Query the database for all TICKET fields for the specific
123 ** ticket whose name is given by the "name" CGI parameter.
124 ** Load the values for all fields into the interpreter.
125 **
126 ** Only load those fields which do not already exist as
127 ** variables.
128 **
129 ** Fields of the TICKET table that begin with "private_" are
130 ** expanded using the db_reveal() function.  If g.perm.RdAddr is
131 ** true, then the db_reveal() function will decode the content
132 ** using the CONCEALED table so that the content legible.
133 ** Otherwise, db_reveal() is a no-op and the content remains
134 ** obscured.
135 */
initializeVariablesFromDb(void)136 static void initializeVariablesFromDb(void){
137   const char *zName;
138   Stmt q;
139   int i, n, size, j;
140 
141   zName = PD("name","-none-");
142   db_prepare(&q, "SELECT datetime(tkt_mtime,toLocal()) AS tkt_datetime, *"
143                  "  FROM ticket WHERE tkt_uuid GLOB '%q*'",
144                  zName);
145   if( db_step(&q)==SQLITE_ROW ){
146     n = db_column_count(&q);
147     for(i=0; i<n; i++){
148       const char *zVal = db_column_text(&q, i);
149       const char *zName = db_column_name(&q, i);
150       char *zRevealed = 0;
151       if( zVal==0 ){
152         zVal = "";
153       }else if( strncmp(zName, "private_", 8)==0 ){
154         zVal = zRevealed = db_reveal(zVal);
155       }
156       if( (j = fieldId(zName))>=0 ){
157         aField[j].zValue = mprintf("%s", zVal);
158       }else if( memcmp(zName, "tkt_", 4)==0 && Th_Fetch(zName, &size)==0 ){
159         Th_Store(zName, zVal);
160       }
161       free(zRevealed);
162     }
163   }
164   db_finalize(&q);
165   for(i=0; i<nField; i++){
166     if( Th_Fetch(aField[i].zName, &size)==0 ){
167       Th_Store(aField[i].zName, aField[i].zValue);
168     }
169   }
170 }
171 
172 /*
173 ** Transfer all CGI parameters to variables in the interpreter.
174 */
initializeVariablesFromCGI(void)175 static void initializeVariablesFromCGI(void){
176   int i;
177   const char *z;
178 
179   for(i=0; (z = cgi_parameter_name(i))!=0; i++){
180     Th_Store(z, P(z));
181   }
182 }
183 
184 /*
185 ** Update an entry of the TICKET and TICKETCHNG tables according to the
186 ** information in the ticket artifact given in p.  Attempt to create
187 ** the appropriate TICKET table entry if tktid is zero.  If tktid is nonzero
188 ** then it will be the ROWID of an existing TICKET entry.
189 **
190 ** Parameter rid is the recordID for the ticket artifact in the BLOB table.
191 **
192 ** Return the new rowid of the TICKET table entry.
193 */
ticket_insert(const Manifest * p,int rid,int tktid)194 static int ticket_insert(const Manifest *p, int rid, int tktid){
195   Blob sql1, sql2, sql3;
196   Stmt q;
197   int i, j;
198   char *aUsed;
199   const char *zMimetype = 0;
200 
201   if( tktid==0 ){
202     db_multi_exec("INSERT INTO ticket(tkt_uuid, tkt_mtime) "
203                   "VALUES(%Q, 0)", p->zTicketUuid);
204     tktid = db_last_insert_rowid();
205   }
206   blob_zero(&sql1);
207   blob_zero(&sql2);
208   blob_zero(&sql3);
209   blob_append_sql(&sql1, "UPDATE OR REPLACE ticket SET tkt_mtime=:mtime");
210   if( haveTicketCTime ){
211     blob_append_sql(&sql1, ", tkt_ctime=coalesce(tkt_ctime,:mtime)");
212   }
213   aUsed = fossil_malloc( nField );
214   memset(aUsed, 0, nField);
215   for(i=0; i<p->nField; i++){
216     const char *zName = p->aField[i].zName;
217     const char *zBaseName = zName[0]=='+' ? zName+1 : zName;
218     j = fieldId(zBaseName);
219     if( j<0 ) continue;
220     aUsed[j] = 1;
221     if( aField[j].mUsed & USEDBY_TICKET ){
222       const char *zUsedByName = zName;
223       if( zUsedByName[0]=='+' ){
224         zUsedByName++;
225         blob_append_sql(&sql1,", \"%w\"=coalesce(\"%w\",'') || %Q",
226                         zUsedByName, zUsedByName, p->aField[i].zValue);
227       }else{
228         blob_append_sql(&sql1,", \"%w\"=%Q", zUsedByName, p->aField[i].zValue);
229       }
230     }
231     if( aField[j].mUsed & USEDBY_TICKETCHNG ){
232       const char *zUsedByName = zName;
233       if( zUsedByName[0]=='+' ){
234         zUsedByName++;
235       }
236       blob_append_sql(&sql2, ",\"%w\"", zUsedByName);
237       blob_append_sql(&sql3, ",%Q", p->aField[i].zValue);
238     }
239     if( strcmp(zBaseName,"mimetype")==0 ){
240       zMimetype = p->aField[i].zValue;
241     }
242   }
243   if( rid>0 ){
244     for(i=0; i<p->nField; i++){
245       const char *zName = p->aField[i].zName;
246       const char *zBaseName = zName[0]=='+' ? zName+1 : zName;
247       j = fieldId(zBaseName);
248       if( j<0 ) continue;
249       backlink_extract(p->aField[i].zValue, zMimetype, rid, BKLNK_TICKET,
250                        p->rDate, i==0);
251     }
252   }
253   blob_append_sql(&sql1, " WHERE tkt_id=%d", tktid);
254   db_prepare(&q, "%s", blob_sql_text(&sql1));
255   db_bind_double(&q, ":mtime", p->rDate);
256   db_step(&q);
257   db_finalize(&q);
258   blob_reset(&sql1);
259   if( blob_size(&sql2)>0 || haveTicketChngRid ){
260     int fromTkt = 0;
261     if( haveTicketChngRid ){
262       blob_append(&sql2, ",tkt_rid", -1);
263       blob_append_sql(&sql3, ",%d", rid);
264     }
265     for(i=0; i<nField; i++){
266       if( aUsed[i]==0
267        && (aField[i].mUsed & USEDBY_BOTH)==USEDBY_BOTH
268       ){
269         const char *z = aField[i].zName;
270         if( z[0]=='+' ) z++;
271         fromTkt = 1;
272         blob_append_sql(&sql2, ",\"%w\"", z);
273         blob_append_sql(&sql3, ",\"%w\"", z);
274       }
275     }
276     if( fromTkt ){
277       db_prepare(&q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%s)"
278                      "SELECT %d,:mtime%s FROM ticket WHERE tkt_id=%d",
279                      blob_sql_text(&sql2), tktid,
280                      blob_sql_text(&sql3), tktid);
281     }else{
282       db_prepare(&q, "INSERT INTO ticketchng(tkt_id,tkt_mtime%s)"
283                      "VALUES(%d,:mtime%s)",
284                      blob_sql_text(&sql2), tktid, blob_sql_text(&sql3));
285     }
286     db_bind_double(&q, ":mtime", p->rDate);
287     db_step(&q);
288     db_finalize(&q);
289   }
290   blob_reset(&sql2);
291   blob_reset(&sql3);
292   fossil_free(aUsed);
293   return tktid;
294 }
295 
296 /*
297 ** Returns non-zero if moderation is required for ticket changes and ticket
298 ** attachments.
299 */
ticket_need_moderation(int localUser)300 int ticket_need_moderation(
301   int localUser /* Are we being called for a local interactive user? */
302 ){
303   /*
304   ** If the FOSSIL_FORCE_TICKET_MODERATION variable is set, *ALL* changes for
305   ** tickets will be required to go through moderation (even those performed
306   ** by the local interactive user via the command line).  This can be useful
307   ** for local (or remote) testing of the moderation subsystem and its impact
308   ** on the contents and status of tickets.
309   */
310   if( fossil_getenv("FOSSIL_FORCE_TICKET_MODERATION")!=0 ){
311     return 1;
312   }
313   if( localUser ){
314     return 0;
315   }
316   return g.perm.ModTkt==0 && db_get_boolean("modreq-tkt",0)==1;
317 }
318 
319 /*
320 ** Rebuild an entire entry in the TICKET table
321 */
ticket_rebuild_entry(const char * zTktUuid)322 void ticket_rebuild_entry(const char *zTktUuid){
323   char *zTag = mprintf("tkt-%s", zTktUuid);
324   int tagid = tag_findid(zTag, 1);
325   Stmt q;
326   Manifest *pTicket;
327   int tktid;
328   int createFlag = 1;
329 
330   fossil_free(zTag);
331   getAllTicketFields();
332   if( haveTicket==0 ) return;
333   tktid = db_int(0, "SELECT tkt_id FROM ticket WHERE tkt_uuid=%Q", zTktUuid);
334   search_doc_touch('t', tktid, 0);
335   if( haveTicketChng ){
336     db_multi_exec("DELETE FROM ticketchng WHERE tkt_id=%d;", tktid);
337   }
338   db_multi_exec("DELETE FROM ticket WHERE tkt_id=%d", tktid);
339   tktid = 0;
340   db_prepare(&q, "SELECT rid FROM tagxref WHERE tagid=%d ORDER BY mtime",tagid);
341   while( db_step(&q)==SQLITE_ROW ){
342     int rid = db_column_int(&q, 0);
343     pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
344     if( pTicket ){
345       tktid = ticket_insert(pTicket, rid, tktid);
346       manifest_ticket_event(rid, pTicket, createFlag, tagid);
347       manifest_destroy(pTicket);
348     }
349     createFlag = 0;
350   }
351   db_finalize(&q);
352 }
353 
354 
355 /*
356 ** Create the TH1 interpreter and load the "common" code.
357 */
ticket_init(void)358 void ticket_init(void){
359   const char *zConfig;
360   Th_FossilInit(TH_INIT_DEFAULT);
361   zConfig = ticket_common_code();
362   Th_Eval(g.interp, 0, zConfig, -1);
363 }
364 
365 /*
366 ** Create the TH1 interpreter and load the "change" code.
367 */
ticket_change(const char * zUuid)368 int ticket_change(const char *zUuid){
369   const char *zConfig;
370   Th_FossilInit(TH_INIT_DEFAULT);
371   Th_Store("uuid", zUuid);
372   zConfig = ticket_change_code();
373   return Th_Eval(g.interp, 0, zConfig, -1);
374 }
375 
376 /*
377 ** An authorizer function for the SQL used to initialize the
378 ** schema for the ticketing system.  Only allow
379 **
380 **     CREATE TABLE
381 **     CREATE INDEX
382 **     CREATE VIEW
383 **     DROP INDEX
384 **     DROP VIEW
385 **
386 ** And for objects in "main" or "repository" whose names
387 ** begin with "ticket" or "fx_".  Also allow
388 **
389 **     INSERT
390 **     UPDATE
391 **     DELETE
392 **
393 ** But only for tables in "main" or "repository" whose names
394 ** begin with "ticket", "sqlite_", or "fx_".
395 **
396 ** Of particular importance for security is that this routine
397 ** disallows data changes on the "config" table, as that could
398 ** allow a malicious server to modify settings in such a way as
399 ** to cause a remote code execution.
400 **
401 ** Use the "fossil test-db-prepare --auth-ticket SQL" command to perform
402 ** manual testing of this authorizer.
403 */
ticket_schema_auth(void * pNErr,int eCode,const char * z0,const char * z1,const char * z2,const char * z3)404 static int ticket_schema_auth(
405   void *pNErr,
406   int eCode,
407   const char *z0,
408   const char *z1,
409   const char *z2,
410   const char *z3
411 ){
412   switch( eCode ){
413     case SQLITE_DROP_VIEW:
414     case SQLITE_CREATE_VIEW:
415     case SQLITE_CREATE_TABLE: {
416       if( sqlite3_stricmp(z2,"main")!=0
417        && sqlite3_stricmp(z2,"repository")!=0
418       ){
419         goto ticket_schema_error;
420       }
421       if( sqlite3_strnicmp(z0,"ticket",6)!=0
422        && sqlite3_strnicmp(z0,"fx_",3)!=0
423       ){
424         goto ticket_schema_error;
425       }
426       break;
427     }
428     case SQLITE_DROP_INDEX:
429     case SQLITE_CREATE_INDEX: {
430       if( sqlite3_stricmp(z2,"main")!=0
431        && sqlite3_stricmp(z2,"repository")!=0
432       ){
433         goto ticket_schema_error;
434       }
435       if( sqlite3_strnicmp(z1,"ticket",6)!=0
436        && sqlite3_strnicmp(z0,"fx_",3)!=0
437       ){
438         goto ticket_schema_error;
439       }
440       break;
441     }
442     case SQLITE_INSERT:
443     case SQLITE_UPDATE:
444     case SQLITE_DELETE: {
445       if( sqlite3_stricmp(z2,"main")!=0
446        && sqlite3_stricmp(z2,"repository")!=0
447       ){
448         goto ticket_schema_error;
449       }
450       if( sqlite3_strnicmp(z0,"ticket",6)!=0
451        && sqlite3_strnicmp(z0,"sqlite_",7)!=0
452        && sqlite3_strnicmp(z0,"fx_",3)!=0
453       ){
454         goto ticket_schema_error;
455       }
456       break;
457     }
458     case SQLITE_FUNCTION:
459     case SQLITE_REINDEX:
460     case SQLITE_TRANSACTION:
461     case SQLITE_READ: {
462       break;
463     }
464     default: {
465       goto ticket_schema_error;
466     }
467   }
468   return SQLITE_OK;
469 
470 ticket_schema_error:
471   if( pNErr ) *(int*)pNErr  = 1;
472   return SQLITE_DENY;
473 }
474 
475 /*
476 ** Activate the ticket schema authorizer. Must be followed by
477 ** an eventual call to ticket_unrestrict_sql().
478 */
ticket_restrict_sql(int * pNErr)479 void ticket_restrict_sql(int * pNErr){
480   db_set_authorizer(ticket_schema_auth,(void*)pNErr,"Ticket-Schema");
481 }
482 /*
483 ** Deactivate the ticket schema authorizer.
484 */
ticket_unrestrict_sql(void)485 void ticket_unrestrict_sql(void){
486   db_clear_authorizer();
487 }
488 
489 
490 /*
491 ** Recreate the TICKET and TICKETCHNG tables.
492 */
ticket_create_table(int separateConnection)493 void ticket_create_table(int separateConnection){
494   char *zSql;
495 
496   db_multi_exec(
497     "DROP TABLE IF EXISTS ticket;"
498     "DROP TABLE IF EXISTS ticketchng;"
499   );
500   zSql = ticket_table_schema();
501   ticket_restrict_sql(0);
502   if( separateConnection ){
503     if( db_transaction_nesting_depth() ) db_end_transaction(0);
504     db_init_database(g.zRepositoryName, zSql, 0);
505   }else{
506     db_multi_exec("%s", zSql/*safe-for-%s*/);
507   }
508   ticket_unrestrict_sql();
509   fossil_free(zSql);
510 }
511 
512 /*
513 ** Repopulate the TICKET and TICKETCHNG tables from scratch using all
514 ** available ticket artifacts.
515 */
ticket_rebuild(void)516 void ticket_rebuild(void){
517   Stmt q;
518   ticket_create_table(1);
519   db_begin_transaction();
520   db_prepare(&q,"SELECT tagname FROM tag WHERE tagname GLOB 'tkt-*'");
521   while( db_step(&q)==SQLITE_ROW ){
522     const char *zName = db_column_text(&q, 0);
523     int len;
524     zName += 4;
525     len = strlen(zName);
526     if( len<20 || !validate16(zName, len) ) continue;
527     ticket_rebuild_entry(zName);
528   }
529   db_finalize(&q);
530   db_end_transaction(0);
531 }
532 
533 /*
534 ** COMMAND: test-ticket-rebuild
535 **
536 ** Usage: %fossil test-ticket-rebuild TICKETID|all
537 **
538 ** Rebuild the TICKET and TICKETCHNG tables for the given ticket ID
539 ** or for ALL.
540 */
test_ticket_rebuild(void)541 void test_ticket_rebuild(void){
542   db_find_and_open_repository(0, 0);
543   if( g.argc!=3 ) usage("TICKETID|all");
544   if( fossil_strcmp(g.argv[2], "all")==0 ){
545     ticket_rebuild();
546   }else{
547     const char *zUuid;
548     zUuid = db_text(0, "SELECT substr(tagname,5) FROM tag"
549                        " WHERE tagname GLOB 'tkt-%q*'", g.argv[2]);
550     if( zUuid==0 ) fossil_fatal("no such ticket: %s", g.argv[2]);
551     ticket_rebuild_entry(zUuid);
552   }
553 }
554 
555 /*
556 ** For trouble-shooting purposes, render a dump of the aField[] table to
557 ** the webpage currently under construction.
558 */
showAllFields(void)559 static void showAllFields(void){
560   int i;
561   @ <div style="color:blue">
562   @ <p>Database fields:</p><ul>
563   for(i=0; i<nField; i++){
564     @ <li>aField[%d(i)].zName = "%h(aField[i].zName)";
565     @ originally = "%h(aField[i].zValue)";
566     @ currently = "%h(PD(aField[i].zName,""))";
567     if( aField[i].zAppend ){
568       @ zAppend = "%h(aField[i].zAppend)";
569     }
570     @ mUsed = %d(aField[i].mUsed);
571   }
572   @ </ul></div>
573 }
574 
575 /*
576 ** WEBPAGE: tktview
577 ** URL:  tktview?name=HASH
578 **
579 ** View a ticket identified by the name= query parameter.
580 ** Other query parameters:
581 **
582 **      tl               Show a timeline of the ticket above the status
583 */
tktview_page(void)584 void tktview_page(void){
585   const char *zScript;
586   char *zFullName;
587   const char *zUuid = PD("name","");
588   int showTimeline = P("tl")!=0;
589 
590   login_check_credentials();
591   if( !g.perm.RdTkt ){ login_needed(g.anon.RdTkt); return; }
592   if( g.anon.WrTkt || g.anon.ApndTkt ){
593     style_submenu_element("Edit", "%R/tktedit?name=%T", PD("name",""));
594   }
595   if( g.perm.Hyperlink ){
596     style_submenu_element("History", "%R/tkthistory/%T", zUuid);
597     style_submenu_element("Check-ins", "%R/tkttimeline/%T?y=ci", zUuid);
598   }
599   if( g.anon.NewTkt ){
600     style_submenu_element("New Ticket", "%R/tktnew");
601   }
602   if( g.anon.ApndTkt && g.anon.Attach ){
603     style_submenu_element("Attach", "%R/attachadd?tkt=%T&from=%R/tktview/%t",
604         zUuid, zUuid);
605   }
606   if( P("plaintext") ){
607     style_submenu_element("Formatted", "%R/tktview/%s", zUuid);
608   }else{
609     style_submenu_element("Plaintext", "%R/tktview/%s?plaintext", zUuid);
610   }
611   style_set_current_feature("tkt");
612   style_header("View Ticket");
613   if( showTimeline ){
614     int tagid = db_int(0,"SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
615                        zUuid);
616     if( tagid ){
617       tkt_draw_timeline(tagid, "a");
618       @ <hr>
619     }else{
620       showTimeline = 0;
621     }
622   }
623   if( !showTimeline && g.perm.Hyperlink ){
624     style_submenu_element("Timeline", "%R/info/%T", zUuid);
625   }
626   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1);
627   ticket_init();
628   initializeVariablesFromCGI();
629   getAllTicketFields();
630   initializeVariablesFromDb();
631   zScript = ticket_viewpage_code();
632   if( P("showfields")!=0 ) showAllFields();
633   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1);
634   safe_html_context(DOCSRC_TICKET);
635   Th_Render(zScript);
636   if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1);
637 
638   zFullName = db_text(0,
639        "SELECT tkt_uuid FROM ticket"
640        " WHERE tkt_uuid GLOB '%q*'", zUuid);
641   if( zFullName ){
642     attachment_list(zFullName, "<hr /><h2>Attachments:</h2><ul>");
643   }
644 
645   style_finish_page();
646 }
647 
648 /*
649 ** TH1 command: append_field FIELD STRING
650 **
651 ** FIELD is the name of a database column to which we might want
652 ** to append text.  STRING is the text to be appended to that
653 ** column.  The append does not actually occur until the
654 ** submit_ticket command is run.
655 */
appendRemarkCmd(Th_Interp * interp,void * p,int argc,const char ** argv,int * argl)656 static int appendRemarkCmd(
657   Th_Interp *interp,
658   void *p,
659   int argc,
660   const char **argv,
661   int *argl
662 ){
663   int idx;
664 
665   if( argc!=3 ){
666     return Th_WrongNumArgs(interp, "append_field FIELD STRING");
667   }
668   if( g.thTrace ){
669     Th_Trace("append_field %#h {%#h}<br />\n",
670               argl[1], argv[1], argl[2], argv[2]);
671   }
672   for(idx=0; idx<nField; idx++){
673     if( memcmp(aField[idx].zName, argv[1], argl[1])==0
674         && aField[idx].zName[argl[1]]==0 ){
675       break;
676     }
677   }
678   if( idx>=nField ){
679     Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
680     return TH_ERROR;
681   }
682   aField[idx].zAppend = mprintf("%.*s", argl[2], argv[2]);
683   return TH_OK;
684 }
685 
686 /*
687 ** Write a ticket into the repository.
688 */
ticket_put(Blob * pTicket,const char * zTktId,int needMod)689 static int ticket_put(
690   Blob *pTicket,           /* The text of the ticket change record */
691   const char *zTktId,      /* The ticket to which this change is applied */
692   int needMod              /* True if moderation is needed */
693 ){
694   int result;
695   int rid;
696   manifest_crosslink_begin();
697   rid = content_put_ex(pTicket, 0, 0, 0, needMod);
698   if( rid==0 ){
699     fossil_fatal("trouble committing ticket: %s", g.zErrMsg);
700   }
701   if( needMod ){
702     moderation_table_create();
703     db_multi_exec(
704       "INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
705       rid, zTktId
706     );
707   }else{
708     db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d);", rid);
709     db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
710   }
711   result = (manifest_crosslink(rid, pTicket, MC_NONE)==0);
712   assert( blob_is_reset(pTicket) );
713   if( !result ){
714     result = manifest_crosslink_end(MC_PERMIT_HOOKS);
715   }else{
716     manifest_crosslink_end(MC_NONE);
717   }
718   return result;
719 }
720 
721 /*
722 ** Subscript command:   submit_ticket
723 **
724 ** Construct and submit a new ticket artifact.  The fields of the artifact
725 ** are the names of the columns in the TICKET table.  The content is
726 ** taken from TH variables.  If the content is unchanged, the field is
727 ** omitted from the artifact.  Fields whose names begin with "private_"
728 ** are concealed using the db_conceal() function.
729 */
submitTicketCmd(Th_Interp * interp,void * pUuid,int argc,const char ** argv,int * argl)730 static int submitTicketCmd(
731   Th_Interp *interp,
732   void *pUuid,
733   int argc,
734   const char **argv,
735   int *argl
736 ){
737   char *zDate;
738   const char *zUuid;
739   int i;
740   int nJ = 0;
741   Blob tktchng, cksum;
742   int needMod;
743 
744   login_verify_csrf_secret();
745   if( !captcha_is_correct(0) ){
746     @ <p class="generalError">Error: Incorrect security code.</p>
747     return TH_OK;
748   }
749   zUuid = (const char *)pUuid;
750   blob_zero(&tktchng);
751   zDate = date_in_standard_format("now");
752   blob_appendf(&tktchng, "D %s\n", zDate);
753   free(zDate);
754   for(i=0; i<nField; i++){
755     if( aField[i].zAppend ){
756       blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
757                    fossilize(aField[i].zAppend, -1));
758       ++nJ;
759     }
760   }
761   for(i=0; i<nField; i++){
762     const char *zValue;
763     int nValue;
764     if( aField[i].zAppend ) continue;
765     zValue = Th_Fetch(aField[i].zName, &nValue);
766     if( zValue ){
767       while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
768       if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
769        || memcmp(zValue, aField[i].zValue, nValue)!=0
770        || strlen(aField[i].zValue)!=nValue
771       ){
772         if( memcmp(aField[i].zName, "private_", 8)==0 ){
773           zValue = db_conceal(zValue, nValue);
774           blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
775         }else{
776           blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
777         }
778         nJ++;
779       }
780     }
781   }
782   if( *(char**)pUuid ){
783     zUuid = db_text(0,
784        "SELECT tkt_uuid FROM ticket WHERE tkt_uuid GLOB '%q*'", P("name")
785     );
786   }else{
787     zUuid = db_text(0, "SELECT lower(hex(randomblob(20)))");
788   }
789   *(const char**)pUuid = zUuid;
790   blob_appendf(&tktchng, "K %s\n", zUuid);
791   blob_appendf(&tktchng, "U %F\n", login_name());
792   md5sum_blob(&tktchng, &cksum);
793   blob_appendf(&tktchng, "Z %b\n", &cksum);
794   if( nJ==0 ){
795     blob_reset(&tktchng);
796     return TH_OK;
797   }
798   needMod = ticket_need_moderation(0);
799   if( g.zPath[0]=='d' ){
800     const char *zNeedMod = needMod ? "required" : "skipped";
801     /* If called from /debug_tktnew or /debug_tktedit... */
802     @ <div style="color:blue">
803     @ <p>Ticket artifact that would have been submitted:</p>
804     @ <blockquote><pre>%h(blob_str(&tktchng))</pre></blockquote>
805     @ <blockquote><pre>Moderation would be %h(zNeedMod).</pre></blockquote>
806     @ </div>
807     @ <hr />
808     return TH_OK;
809   }else{
810     if( g.thTrace ){
811       Th_Trace("submit_ticket {\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
812                "}<br />\n",
813          blob_str(&tktchng));
814     }
815     ticket_put(&tktchng, zUuid, needMod);
816   }
817   return ticket_change(zUuid);
818 }
819 
820 
821 /*
822 ** WEBPAGE: tktnew
823 ** WEBPAGE: debug_tktnew
824 **
825 ** Enter a new ticket.  The tktnew_template script in the ticket
826 ** configuration is used.  The /tktnew page is the official ticket
827 ** entry page.  The /debug_tktnew page is used for debugging the
828 ** tktnew_template in the ticket configuration.  /debug_tktnew works
829 ** just like /tktnew except that it does not really save the new ticket
830 ** when you press submit - it just prints the ticket artifact at the
831 ** top of the screen.
832 */
tktnew_page(void)833 void tktnew_page(void){
834   const char *zScript;
835   char *zNewUuid = 0;
836 
837   login_check_credentials();
838   if( !g.perm.NewTkt ){ login_needed(g.anon.NewTkt); return; }
839   if( P("cancel") ){
840     cgi_redirect("home");
841   }
842   style_set_current_feature("tkt");
843   style_header("New Ticket");
844   ticket_standard_submenu(T_ALL_BUT(T_NEW));
845   if( g.thTrace ) Th_Trace("BEGIN_TKTNEW<br />\n", -1);
846   ticket_init();
847   initializeVariablesFromCGI();
848   getAllTicketFields();
849   initializeVariablesFromDb();
850   if( g.zPath[0]=='d' ) showAllFields();
851   form_begin(0, "%R/%s", g.zPath);
852   login_insert_csrf_secret();
853   if( P("date_override") && g.perm.Setup ){
854     @ <input type="hidden" name="date_override" value="%h(P("date_override"))">
855   }
856   zScript = ticket_newpage_code();
857   Th_Store("login", login_name());
858   Th_Store("date", db_text(0, "SELECT datetime('now')"));
859   Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
860                    (void*)&zNewUuid, 0);
861   if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br />\n", -1);
862   if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
863     cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
864     return;
865   }
866   captcha_generate(0);
867   @ </form>
868   if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1);
869   style_finish_page();
870 }
871 
872 /*
873 ** WEBPAGE: tktedit
874 ** WEBPAGE: debug_tktedit
875 **
876 ** Edit a ticket.  The ticket is identified by the name CGI parameter.
877 ** /tktedit is the official page.  The /debug_tktedit page does the same
878 ** thing except that it does not save the ticket change record when you
879 ** press submit - it instead prints the ticket change record at the top
880 ** of the page.  The /debug_tktedit page is intended to be used when
881 ** debugging ticket configurations.
882 */
tktedit_page(void)883 void tktedit_page(void){
884   const char *zScript;
885   int nName;
886   const char *zName;
887   int nRec;
888 
889   login_check_credentials();
890   if( !g.perm.ApndTkt && !g.perm.WrTkt ){
891     login_needed(g.anon.ApndTkt || g.anon.WrTkt);
892     return;
893   }
894   zName = P("name");
895   if( P("cancel") ){
896     cgi_redirectf("tktview?name=%T", zName);
897   }
898   style_set_current_feature("tkt");
899   style_header("Edit Ticket");
900   if( zName==0 || (nName = strlen(zName))<4 || nName>HNAME_LEN_SHA1
901           || !validate16(zName,nName) ){
902     @ <span class="tktError">Not a valid ticket id: "%h(zName)"</span>
903     style_finish_page();
904     return;
905   }
906   nRec = db_int(0, "SELECT count(*) FROM ticket WHERE tkt_uuid GLOB '%q*'",
907                 zName);
908   if( nRec==0 ){
909     @ <span class="tktError">No such ticket: "%h(zName)"</span>
910     style_finish_page();
911     return;
912   }
913   if( nRec>1 ){
914     @ <span class="tktError">%d(nRec) tickets begin with:
915     @ "%h(zName)"</span>
916     style_finish_page();
917     return;
918   }
919   if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br />\n", -1);
920   ticket_init();
921   getAllTicketFields();
922   initializeVariablesFromCGI();
923   initializeVariablesFromDb();
924   if( g.zPath[0]=='d' ) showAllFields();
925   form_begin(0, "%R/%s", g.zPath);
926   @ <input type="hidden" name="name" value="%s(zName)" />
927   login_insert_csrf_secret();
928   zScript = ticket_editpage_code();
929   Th_Store("login", login_name());
930   Th_Store("date", db_text(0, "SELECT datetime('now')"));
931   Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
932   Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
933   if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br />\n", -1);
934   if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
935     cgi_redirect(mprintf("%R/tktview/%s", zName));
936     return;
937   }
938   captcha_generate(0);
939   @ </form>
940   if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br />\n", -1);
941   style_finish_page();
942 }
943 
944 /*
945 ** Check the ticket table schema in zSchema to see if it appears to
946 ** be well-formed.  If everything is OK, return NULL.  If something is
947 ** amiss, then return a pointer to a string (obtained from malloc) that
948 ** describes the problem.
949 */
ticket_schema_check(const char * zSchema)950 char *ticket_schema_check(const char *zSchema){
951   char *zErr = 0;
952   int rc;
953   sqlite3 *db;
954   rc = sqlite3_open(":memory:", &db);
955   if( rc==SQLITE_OK ){
956     rc = sqlite3_exec(db, zSchema, 0, 0, &zErr);
957     if( rc!=SQLITE_OK ){
958       sqlite3_close(db);
959       return zErr;
960     }
961     rc = sqlite3_exec(db, "SELECT tkt_id, tkt_uuid, tkt_mtime FROM ticket",
962                       0, 0, 0);
963     if( rc!=SQLITE_OK ){
964       zErr = mprintf("schema fails to define valid a TICKET "
965                      "table containing all required fields");
966     }else{
967       rc = sqlite3_exec(db, "SELECT tkt_id, tkt_mtime FROM ticketchng", 0,0,0);
968       if( rc!=SQLITE_OK ){
969         zErr = mprintf("schema fails to define valid a TICKETCHNG "
970                        "table containing all required fields");
971       }
972     }
973     sqlite3_close(db);
974   }
975   return zErr;
976 }
977 
978 /*
979 ** Draw a timeline for a ticket with tag.tagid given by the tagid
980 ** parameter.
981 **
982 ** If zType[0]=='c' then only show check-ins associated with the
983 ** ticket.  For any other value of zType, show all events associated
984 ** with the ticket.
985 */
tkt_draw_timeline(int tagid,const char * zType)986 void tkt_draw_timeline(int tagid, const char *zType){
987   Stmt q;
988   char *zFullUuid;
989   char *zSQL;
990   zFullUuid = db_text(0, "SELECT substr(tagname, 5) FROM tag WHERE tagid=%d",
991                          tagid);
992   if( zType[0]=='c' ){
993     zSQL = mprintf(
994          "%s AND event.objid IN "
995          " (SELECT srcid FROM backlink WHERE target GLOB '%.4s*' "
996                                          "AND srctype=0 "
997                                          "AND '%s' GLOB (target||'*')) "
998          "ORDER BY mtime DESC",
999          timeline_query_for_www(), zFullUuid, zFullUuid
1000     );
1001   }else{
1002     zSQL = mprintf(
1003          "%s AND event.objid IN "
1004          "  (SELECT rid FROM tagxref WHERE tagid=%d"
1005          "   UNION"
1006          "   SELECT CASE srctype WHEN 2 THEN"
1007                  " (SELECT rid FROM tagxref WHERE tagid=backlink.srcid"
1008                  " ORDER BY mtime DESC LIMIT 1)"
1009                  " ELSE srcid END"
1010          "     FROM backlink"
1011                   " WHERE target GLOB '%.4s*'"
1012                   "   AND '%s' GLOB (target||'*')"
1013          "   UNION SELECT attachid FROM attachment"
1014                   " WHERE target=%Q) "
1015          "ORDER BY mtime DESC",
1016          timeline_query_for_www(), tagid, zFullUuid, zFullUuid, zFullUuid
1017     );
1018   }
1019   db_prepare(&q, "%z", zSQL/*safe-for-%s*/);
1020   www_print_timeline(&q,
1021     TIMELINE_ARTID | TIMELINE_DISJOINT | TIMELINE_GRAPH | TIMELINE_NOTKT |
1022     TIMELINE_REFS,
1023     0, 0, 0, 0, 0, 0);
1024   db_finalize(&q);
1025   fossil_free(zFullUuid);
1026 }
1027 
1028 /*
1029 ** WEBPAGE: tkttimeline
1030 ** URL: /tkttimeline/TICKETUUID
1031 **
1032 ** Show the change history for a single ticket in timeline format.
1033 **
1034 ** Query parameters:
1035 **
1036 **     y=ci          Show only check-ins associated with the ticket
1037 */
tkttimeline_page(void)1038 void tkttimeline_page(void){
1039   char *zTitle;
1040   const char *zUuid;
1041   int tagid;
1042   char zGlobPattern[50];
1043   const char *zType;
1044 
1045   login_check_credentials();
1046   if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1047     login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1048     return;
1049   }
1050   zUuid = PD("name","");
1051   zType = PD("y","a");
1052   if( zType[0]!='c' ){
1053     style_submenu_element("Check-ins", "%R/tkttimeline?name=%T&y=ci", zUuid);
1054   }else{
1055     style_submenu_element("Timeline", "%R/tkttimeline?name=%T", zUuid);
1056   }
1057   style_submenu_element("History", "%R/tkthistory/%s", zUuid);
1058   style_submenu_element("Status", "%R/info/%s", zUuid);
1059   if( zType[0]=='c' ){
1060     zTitle = mprintf("Check-ins Associated With Ticket %h", zUuid);
1061   }else{
1062     zTitle = mprintf("Timeline Of Ticket %h", zUuid);
1063   }
1064   style_set_current_feature("tkt");
1065   style_header("%z", zTitle);
1066 
1067   sqlite3_snprintf(6, zGlobPattern, "%s", zUuid);
1068   canonical16(zGlobPattern, strlen(zGlobPattern));
1069   tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
1070   if( tagid==0 ){
1071     @ No such ticket: %h(zUuid)
1072     style_finish_page();
1073     return;
1074   }
1075   tkt_draw_timeline(tagid, zType);
1076   style_finish_page();
1077 }
1078 
1079 /*
1080 ** WEBPAGE: tkthistory
1081 ** URL: /tkthistory?name=TICKETUUID
1082 **
1083 ** Show the complete change history for a single ticket.  Or (to put it
1084 ** another way) show a list of artifacts associated with a single ticket.
1085 **
1086 ** By default, the artifacts are decoded and formatted.  Text fields
1087 ** are formatted as text/plain, since in the general case Fossil does
1088 ** not have knowledge of the encoding.  If the "raw" query parameter
1089 ** is present, then the undecoded and unformatted text of each artifact
1090 ** is displayed.
1091 */
tkthistory_page(void)1092 void tkthistory_page(void){
1093   Stmt q;
1094   char *zTitle;
1095   const char *zUuid;
1096   int tagid;
1097   int nChng = 0;
1098 
1099   login_check_credentials();
1100   if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1101     login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1102     return;
1103   }
1104   zUuid = PD("name","");
1105   zTitle = mprintf("History Of Ticket %h", zUuid);
1106   style_submenu_element("Status", "%R/info/%s", zUuid);
1107   style_submenu_element("Check-ins", "%R/tkttimeline?name=%s&y=ci", zUuid);
1108   style_submenu_element("Timeline", "%R/tkttimeline?name=%s", zUuid);
1109   if( P("raw")!=0 ){
1110     style_submenu_element("Decoded", "%R/tkthistory/%s", zUuid);
1111   }else if( g.perm.Admin ){
1112     style_submenu_element("Raw", "%R/tkthistory/%s?raw", zUuid);
1113   }
1114   style_set_current_feature("tkt");
1115   style_header("%z", zTitle);
1116 
1117   tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
1118   if( tagid==0 ){
1119     @ No such ticket: %h(zUuid)
1120     style_finish_page();
1121     return;
1122   }
1123   if( P("raw")!=0 ){
1124     @ <h2>Raw Artifacts Associated With Ticket %h(zUuid)</h2>
1125   }else{
1126     @ <h2>Artifacts Associated With Ticket %h(zUuid)</h2>
1127   }
1128   db_prepare(&q,
1129     "SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL"
1130     "  FROM event, blob"
1131     " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
1132     "   AND blob.rid=event.objid"
1133     " UNION "
1134     "SELECT datetime(mtime,toLocal()), attachid, uuid, src, filename, user"
1135     "  FROM attachment, blob"
1136     " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
1137     "   AND blob.rid=attachid"
1138     " ORDER BY 1",
1139     tagid, tagid
1140   );
1141   for(nChng=0; db_step(&q)==SQLITE_ROW; nChng++){
1142     Manifest *pTicket;
1143     const char *zDate = db_column_text(&q, 0);
1144     int rid = db_column_int(&q, 1);
1145     const char *zChngUuid = db_column_text(&q, 2);
1146     const char *zFile = db_column_text(&q, 4);
1147     if( nChng==0 ){
1148       @ <ol>
1149     }
1150     if( zFile!=0 ){
1151       const char *zSrc = db_column_text(&q, 3);
1152       const char *zUser = db_column_text(&q, 5);
1153       if( zSrc==0 || zSrc[0]==0 ){
1154         @
1155         @ <li><p>Delete attachment "%h(zFile)"
1156       }else{
1157         @
1158         @ <li><p>Add attachment
1159         @ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)</a>"
1160       }
1161       @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1162       @ (rid %d(rid)) by
1163       hyperlink_to_user(zUser,zDate," on");
1164       hyperlink_to_date(zDate, ".</p>");
1165     }else{
1166       pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1167       if( pTicket ){
1168         @
1169         @ <li><p>Ticket change
1170         @ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]
1171         @ (rid %d(rid)) by
1172         hyperlink_to_user(pTicket->zUser,zDate," on");
1173         hyperlink_to_date(zDate, ":");
1174         @ </p>
1175         if( P("raw")!=0 ){
1176           Blob c;
1177           content_get(rid, &c);
1178           @ <blockquote><pre>
1179           @ %h(blob_str(&c))
1180           @ </pre></blockquote>
1181           blob_reset(&c);
1182         }else{
1183           ticket_output_change_artifact(pTicket, "a", nChng);
1184         }
1185       }
1186       manifest_destroy(pTicket);
1187     }
1188   }
1189   db_finalize(&q);
1190   if( nChng ){
1191     @ </ol>
1192   }
1193   style_finish_page();
1194 }
1195 
1196 /*
1197 ** Return TRUE if the given BLOB contains a newline character.
1198 */
contains_newline(Blob * p)1199 static int contains_newline(Blob *p){
1200   const char *z = blob_str(p);
1201   while( *z ){
1202     if( *z=='\n' ) return 1;
1203     z++;
1204   }
1205   return 0;
1206 }
1207 
1208 /*
1209 ** The pTkt object is a ticket change artifact.  Output a detailed
1210 ** description of this object.
1211 */
ticket_output_change_artifact(Manifest * pTkt,const char * zListType,int n)1212 void ticket_output_change_artifact(
1213   Manifest *pTkt,           /* Parsed artifact for the ticket change */
1214   const char *zListType,    /* Which type of list */
1215   int n                     /* Which ticket change is this */
1216 ){
1217   int i;
1218   if( zListType==0 ) zListType = "1";
1219   getAllTicketFields();
1220   @ <ol type="%s(zListType)">
1221   for(i=0; i<pTkt->nField; i++){
1222     Blob val;
1223     const char *z, *zX;
1224     int id;
1225     z = pTkt->aField[i].zName;
1226     blob_set(&val, pTkt->aField[i].zValue);
1227     zX = z[0]=='+' ? z+1 : z;
1228     id = fieldId(zX);
1229     @ <li>\
1230     if( id<0 ){
1231       @ Untracked field %h(zX):
1232     }else if( aField[id].mUsed==USEDBY_TICKETCHNG ){
1233       @ %h(zX):
1234     }else if( n==0 ){
1235       @ %h(zX) initialized to:
1236     }else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){
1237       @ Appended to %h(zX):
1238     }else{
1239       @ %h(zX) changed to:
1240     }
1241     if( blob_size(&val)>50 || contains_newline(&val) ){
1242       @ <blockquote><pre class='verbatim'>
1243       @ %h(blob_str(&val))
1244       @ </pre></blockquote></li>
1245     }else{
1246       @ "%h(blob_str(&val))"</li>
1247     }
1248     blob_reset(&val);
1249   }
1250   @ </ol>
1251 }
1252 
1253 /*
1254 ** COMMAND: ticket*
1255 **
1256 ** Usage: %fossil ticket SUBCOMMAND ...
1257 **
1258 ** Run various subcommands to control tickets
1259 **
1260 ** > fossil ticket show (REPORTTITLE|REPORTNR) ?TICKETFILTER? ?OPTIONS?
1261 **
1262 **     Options:
1263 **       -l|--limit LIMITCHAR
1264 **       -q|--quote
1265 **       -R|--repository REPO
1266 **
1267 **     Run the ticket report, identified by the report format title
1268 **     used in the GUI. The data is written as flat file on stdout,
1269 **     using TAB as separator. The separator can be changed using
1270 **     the -l or --limit option.
1271 **
1272 **     If TICKETFILTER is given on the commandline, the query is
1273 **     limited with a new WHERE-condition.
1274 **       example:  Report lists a column # with the uuid
1275 **                 TICKETFILTER may be [#]='uuuuuuuuu'
1276 **       example:  Report only lists rows with status not open
1277 **                 TICKETFILTER: status != 'open'
1278 **
1279 **     If --quote is used, the tickets are encoded by quoting special
1280 **     chars (space -> \\s, tab -> \\t, newline -> \\n, cr -> \\r,
1281 **     formfeed -> \\f, vtab -> \\v, nul -> \\0, \\ -> \\\\).
1282 **     Otherwise, the simplified encoding as on the show report raw page
1283 **     in the GUI is used. This has no effect in JSON mode.
1284 **
1285 **     Instead of the report title it's possible to use the report
1286 **     number; the special report number 0 lists all columns defined in
1287 **     the ticket table.
1288 **
1289 ** > fossil ticket list fields
1290 ** > fossil ticket ls fields
1291 **
1292 **     List all fields defined for ticket in the fossil repository.
1293 **
1294 ** > fossil ticket list reports
1295 ** > fossil ticket ls reports
1296 **
1297 **     List all ticket reports defined in the fossil repository.
1298 **
1299 ** > fossil ticket set TICKETUUID (FIELD VALUE)+ ?-q|--quote?
1300 ** > fossil ticket change TICKETUUID (FIELD VALUE)+ ?-q|--quote?
1301 **
1302 **     Change ticket identified by TICKETUUID to set the values of
1303 **     each field FIELD to VALUE.
1304 **
1305 **     Field names as defined in the TICKET table.  By default, these
1306 **     names include: type, status, subsystem, priority, severity, foundin,
1307 **     resolution, title, and comment, but other field names can be added
1308 **     or substituted in customized installations.
1309 **
1310 **     If you use +FIELD, the VALUE is appended to the field FIELD.  You
1311 **     can use more than one field/value pair on the commandline.  Using
1312 **     --quote enables the special character decoding as in "ticket
1313 **     show", which allows setting multiline text or text with special
1314 **     characters.
1315 **
1316 ** > fossil ticket add FIELD VALUE ?FIELD VALUE .. ? ?-q|--quote?
1317 **
1318 **     Like set, but create a new ticket with the given values.
1319 **
1320 ** > fossil ticket history TICKETUUID
1321 **
1322 **     Show the complete change history for the ticket
1323 **
1324 ** Note that the values in set|add are not validated against the
1325 ** definitions given in "Ticket Common Script".
1326 */
ticket_cmd(void)1327 void ticket_cmd(void){
1328   int n;
1329   const char *zUser;
1330   const char *zDate;
1331   const char *zTktUuid;
1332 
1333   /* do some ints, we want to be inside a checkout */
1334   db_find_and_open_repository(0, 0);
1335   user_select();
1336 
1337   zUser = find_option("user-override",0,1);
1338   if( zUser==0 ) zUser = login_name();
1339   zDate = find_option("date-override",0,1);
1340   if( zDate==0 ) zDate = "now";
1341   zDate = date_in_standard_format(zDate);
1342   zTktUuid = find_option("uuid-override",0,1);
1343   if( zTktUuid && (strlen(zTktUuid)!=40 || !validate16(zTktUuid,40)) ){
1344     fossil_fatal("invalid --uuid-override: must be 40 characters of hex");
1345   }
1346 
1347   /*
1348   ** Check that the user exists.
1349   */
1350   if( !db_exists("SELECT 1 FROM user WHERE login=%Q", zUser) ){
1351     fossil_fatal("no such user: %s", zUser);
1352   }
1353 
1354   if( g.argc<3 ){
1355     usage("add|change|list|set|show|history");
1356   }
1357   n = strlen(g.argv[2]);
1358   if( n==1 && g.argv[2][0]=='s' ){
1359     /* set/show cannot be distinguished, so show the usage */
1360     usage("add|change|list|set|show|history");
1361   }
1362   if(( strncmp(g.argv[2],"list",n)==0 ) || ( strncmp(g.argv[2],"ls",n)==0 )){
1363     if( g.argc==3 ){
1364       usage("list fields|reports");
1365     }else{
1366       n = strlen(g.argv[3]);
1367       if( !strncmp(g.argv[3],"fields",n) ){
1368         /* simply show all field names */
1369         int i;
1370 
1371         /* read all available ticket fields */
1372         getAllTicketFields();
1373         for(i=0; i<nField; i++){
1374           printf("%s\n",aField[i].zName);
1375         }
1376       }else if( !strncmp(g.argv[3],"reports",n) ){
1377         rpt_list_reports();
1378       }else{
1379         fossil_fatal("unknown ticket list option '%s'!",g.argv[3]);
1380       }
1381     }
1382   }else{
1383     /* add a new ticket or set fields on existing tickets */
1384     tTktShowEncoding tktEncoding;
1385 
1386     tktEncoding = find_option("quote","q",0) ? tktFossilize : tktNoTab;
1387 
1388     if( strncmp(g.argv[2],"show",n)==0 ){
1389       if( g.argc==3 ){
1390         usage("show REPORTNR");
1391       }else{
1392         const char *zRep = 0;
1393         const char *zSep = 0;
1394         const char *zFilterUuid = 0;
1395         zSep = find_option("limit","l",1);
1396         zRep = g.argv[3];
1397         if( !strcmp(zRep,"0") ){
1398           zRep = 0;
1399         }
1400         if( g.argc>4 ){
1401           zFilterUuid = g.argv[4];
1402         }
1403         rptshow( zRep, zSep, zFilterUuid, tktEncoding );
1404       }
1405     }else{
1406       /* add a new ticket or update an existing ticket */
1407       enum { set,add,history,err } eCmd = err;
1408       int i = 0;
1409       Blob tktchng, cksum;
1410 
1411       /* get command type (set/add) and get uuid, if needed for set */
1412       if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
1413          strncmp(g.argv[2],"history",n)==0 ){
1414         if( strncmp(g.argv[2],"history",n)==0 ){
1415           eCmd = history;
1416         }else{
1417           eCmd = set;
1418         }
1419         if( g.argc==3 ){
1420           usage("set|change|history TICKETUUID");
1421         }
1422         zTktUuid = db_text(0,
1423           "SELECT tkt_uuid FROM ticket WHERE tkt_uuid GLOB '%q*'", g.argv[3]
1424         );
1425         if( !zTktUuid ){
1426           fossil_fatal("unknown ticket: '%s'!",g.argv[3]);
1427         }
1428         i=4;
1429       }else if( strncmp(g.argv[2],"add",n)==0 ){
1430         eCmd = add;
1431         i = 3;
1432         if( zTktUuid==0 ){
1433           zTktUuid = db_text(0, "SELECT lower(hex(randomblob(20)))");
1434         }
1435       }
1436       /* none of set/add, so show the usage! */
1437       if( eCmd==err ){
1438         usage("add|fieldlist|set|show|history");
1439       }
1440 
1441       /* we just handle history separately here, does not get out */
1442       if( eCmd==history ){
1443         Stmt q;
1444         int tagid;
1445 
1446         if( i != g.argc ){
1447           fossil_fatal("no other parameters expected to %s!",g.argv[2]);
1448         }
1449         tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
1450                        zTktUuid);
1451         if( tagid==0 ){
1452           fossil_fatal("no such ticket %h", zTktUuid);
1453         }
1454         db_prepare(&q,
1455           "SELECT datetime(mtime,toLocal()), objid, NULL, NULL, NULL"
1456           "  FROM event, blob"
1457           " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
1458           "   AND blob.rid=event.objid"
1459           " UNION "
1460           "SELECT datetime(mtime,toLocal()), attachid, filename, "
1461           "       src, user"
1462           "  FROM attachment, blob"
1463           " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
1464           "   AND blob.rid=attachid"
1465           " ORDER BY 1 DESC",
1466           tagid, tagid
1467         );
1468         while( db_step(&q)==SQLITE_ROW ){
1469           Manifest *pTicket;
1470           const char *zDate = db_column_text(&q, 0);
1471           int rid = db_column_int(&q, 1);
1472           const char *zFile = db_column_text(&q, 2);
1473           if( zFile!=0 ){
1474             const char *zSrc = db_column_text(&q, 3);
1475             const char *zUser = db_column_text(&q, 4);
1476             if( zSrc==0 || zSrc[0]==0 ){
1477               fossil_print("Delete attachment %s\n", zFile);
1478             }else{
1479               fossil_print("Add attachment %s\n", zFile);
1480             }
1481             fossil_print(" by %s on %s\n", zUser, zDate);
1482           }else{
1483             pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1484             if( pTicket ){
1485               int i;
1486 
1487               fossil_print("Ticket Change by %s on %s:\n",
1488                            pTicket->zUser, zDate);
1489               for(i=0; i<pTicket->nField; i++){
1490                 Blob val;
1491                 const char *z;
1492                 z = pTicket->aField[i].zName;
1493                 blob_set(&val, pTicket->aField[i].zValue);
1494                 if( z[0]=='+' ){
1495                   fossil_print("  Append to ");
1496             z++;
1497           }else{
1498             fossil_print("  Change ");
1499           }
1500           fossil_print("%h: ",z);
1501           if( blob_size(&val)>50 || contains_newline(&val)) {
1502                   fossil_print("\n    ");
1503                   comment_print(blob_str(&val),0,4,-1,get_comment_format());
1504                 }else{
1505                   fossil_print("%s\n",blob_str(&val));
1506                 }
1507                 blob_reset(&val);
1508               }
1509             }
1510             manifest_destroy(pTicket);
1511           }
1512         }
1513         db_finalize(&q);
1514         return;
1515       }
1516       /* read all given ticket field/value pairs from command line */
1517       if( i==g.argc ){
1518         fossil_fatal("empty %s command aborted!",g.argv[2]);
1519       }
1520       getAllTicketFields();
1521       /* read command-line and assign fields in the aField[].zValue array */
1522       while( i<g.argc ){
1523         char *zFName;
1524         char *zFValue;
1525         int j;
1526         int append = 0;
1527 
1528         zFName = g.argv[i++];
1529         if( i==g.argc ){
1530           fossil_fatal("missing value for '%s'!",zFName);
1531         }
1532         zFValue = g.argv[i++];
1533         if( tktEncoding == tktFossilize ){
1534           zFValue=mprintf("%s",zFValue);
1535           defossilize(zFValue);
1536         }
1537         append = (zFName[0] == '+');
1538         if( append ){
1539           zFName++;
1540         }
1541         j = fieldId(zFName);
1542         if( j == -1 ){
1543           fossil_fatal("unknown field name '%s'!",zFName);
1544         }else{
1545           if( append ){
1546             aField[j].zAppend = zFValue;
1547           }else{
1548             aField[j].zValue = zFValue;
1549           }
1550         }
1551       }
1552 
1553       /* now add the needed artifacts to the repository */
1554       blob_zero(&tktchng);
1555       /* add the time to the ticket manifest */
1556       blob_appendf(&tktchng, "D %s\n", zDate);
1557       /* append defined elements */
1558       for(i=0; i<nField; i++){
1559         char *zValue = 0;
1560         char *zPfx;
1561 
1562         if( aField[i].zAppend && aField[i].zAppend[0] ){
1563           zPfx = " +";
1564           zValue = aField[i].zAppend;
1565         }else if( aField[i].zValue && aField[i].zValue[0] ){
1566           zPfx = " ";
1567           zValue = aField[i].zValue;
1568         }else{
1569           continue;
1570         }
1571         if( memcmp(aField[i].zName, "private_", 8)==0 ){
1572           zValue = db_conceal(zValue, strlen(zValue));
1573           blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
1574         }else{
1575           blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
1576                        aField[i].zName, strlen(zValue), zValue);
1577         }
1578       }
1579       blob_appendf(&tktchng, "K %s\n", zTktUuid);
1580       blob_appendf(&tktchng, "U %F\n", zUser);
1581       md5sum_blob(&tktchng, &cksum);
1582       blob_appendf(&tktchng, "Z %b\n", &cksum);
1583       if( ticket_put(&tktchng, zTktUuid, ticket_need_moderation(1))==0 ){
1584         fossil_fatal("%s", g.zErrMsg);
1585       }else{
1586         fossil_print("ticket %s succeeded for %s\n",
1587              (eCmd==set?"set":"add"),zTktUuid);
1588       }
1589     }
1590   }
1591 }
1592 
1593 
1594 #if INTERFACE
1595 /* Standard submenu items for wiki pages */
1596 #define T_SRCH        0x00001
1597 #define T_REPLIST     0x00002
1598 #define T_NEW         0x00004
1599 #define T_ALL         0x00007
1600 #define T_ALL_BUT(x)  (T_ALL&~(x))
1601 #endif
1602 
1603 /*
1604 ** Add some standard submenu elements for ticket screens.
1605 */
ticket_standard_submenu(unsigned int ok)1606 void ticket_standard_submenu(unsigned int ok){
1607   if( (ok & T_SRCH)!=0 && search_restrict(SRCH_TKT)!=0 ){
1608     style_submenu_element("Search", "%R/tktsrch");
1609   }
1610   if( (ok & T_REPLIST)!=0 ){
1611     style_submenu_element("Reports", "%R/reportlist");
1612   }
1613   if( (ok & T_NEW)!=0 && g.anon.NewTkt ){
1614     style_submenu_element("New", "%R/tktnew");
1615   }
1616 }
1617 
1618 /*
1619 ** WEBPAGE: ticket
1620 **
1621 ** This is intended to be the primary "Ticket" page.  Render as
1622 ** either ticket-search (if search is enabled) or as the
1623 ** /reportlist page (if ticket search is disabled).
1624 */
tkt_home_page(void)1625 void tkt_home_page(void){
1626   login_check_credentials();
1627   if( search_restrict(SRCH_TKT)!=0 ){
1628     tkt_srchpage();
1629   }else{
1630     view_list();
1631   }
1632 }
1633 
1634 /*
1635 ** WEBPAGE: tktsrch
1636 ** Usage:  /tktsrch?s=PATTERN
1637 **
1638 ** Full-text search of all current tickets
1639 */
tkt_srchpage(void)1640 void tkt_srchpage(void){
1641   char *defaultReport;
1642   login_check_credentials();
1643   style_set_current_feature("tkt");
1644   style_header("Ticket Search");
1645   ticket_standard_submenu(T_ALL_BUT(T_SRCH));
1646   if( !search_screen(SRCH_TKT, 0) ){
1647     defaultReport = db_get("ticket-default-report", 0);
1648     if( defaultReport ){
1649       rptview_page_content(defaultReport, 0, 0);
1650     }
1651   }
1652   style_finish_page();
1653 }
1654