Fossil SCM

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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button