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