Fossil SCM

fossil-scm / src / tkt.c
Blame History Raw 1922 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
if( g.anon.ApndTkt && g.anon.Attach ){
755
style_submenu_element("Attach", "%R/attachadd?tkt=%T&from=%R/tktview/%t",
756
zUuid, zUuid);
757
}
758
if( P("plaintext") ){
759
style_submenu_element("Formatted", "%R/tktview/%s", zUuid);
760
}else{
761
style_submenu_element("Plaintext", "%R/tktview/%s?plaintext", zUuid);
762
}
763
style_set_current_feature("tkt");
764
style_header("View Ticket");
765
if( showTimeline ){
766
int tagid = db_int(0,"SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
767
zUuid);
768
if( tagid ){
769
tkt_draw_timeline(tagid, "a");
770
@ <hr>
771
}else{
772
showTimeline = 0;
773
}
774
}
775
if( !showTimeline && g.perm.Hyperlink ){
776
style_submenu_element("Timeline", "%R/info/%T", zUuid);
777
}
778
zFullName = db_text(0,
779
"SELECT tkt_uuid FROM ticket"
780
" WHERE tkt_uuid GLOB '%q*'", zUuid);
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
attachment_list(zFullName, "<h2>Attachments:</h2>", 1);
795
}
796
797
builtin_fossil_js_bundle_or("dom", "storage", NULL);
798
builtin_request_js("fossil.page.ticket.js");
799
builtin_fulfill_js_requests();
800
801
style_finish_page();
802
}
803
804
/*
805
** TH1 command: append_field FIELD STRING
806
**
807
** FIELD is the name of a database column to which we might want
808
** to append text. STRING is the text to be appended to that
809
** column. The append does not actually occur until the
810
** submit_ticket command is run.
811
*/
812
static int appendRemarkCmd(
813
Th_Interp *interp,
814
void *p,
815
int argc,
816
const char **argv,
817
int *argl
818
){
819
int idx;
820
821
if( argc!=3 ){
822
return Th_WrongNumArgs(interp, "append_field FIELD STRING");
823
}
824
if( g.thTrace ){
825
Th_Trace("append_field %#h {%#h}<br>\n",
826
TH1_LEN(argl[1]), argv[1], TH1_LEN(argl[2]), argv[2]);
827
}
828
for(idx=0; idx<nField; idx++){
829
if( memcmp(aField[idx].zName, argv[1], TH1_LEN(argl[1]))==0
830
&& aField[idx].zName[TH1_LEN(argl[1])]==0 ){
831
break;
832
}
833
}
834
if( idx>=nField ){
835
Th_ErrorMessage(g.interp, "no such TICKET column: ", argv[1], argl[1]);
836
return TH_ERROR;
837
}
838
aField[idx].zAppend = mprintf("%.*s", argl[2], argv[2]);
839
return TH_OK;
840
}
841
842
/*
843
** Write a ticket into the repository.
844
** Upon reassignment of fields try to delta-compress an artifact against
845
** all artifacts that are referenced in the corresponding zBsln fields.
846
*/
847
static int ticket_put(
848
Blob *pTicket, /* The text of the ticket change record */
849
const char *zTktId, /* The ticket to which this change is applied */
850
const char *aUsed, /* Indicators for fields' modifications */
851
int needMod /* True if moderation is needed */
852
){
853
int result;
854
int rid;
855
manifest_crosslink_begin();
856
rid = content_put_ex(pTicket, 0, 0, 0, needMod);
857
if( rid==0 ){
858
fossil_fatal("trouble committing ticket: %s", g.zErrMsg);
859
}
860
if( nTicketBslns ){
861
int i, s, buf[8], nSrc=0, *aSrc=&(buf[0]);
862
if( nTicketBslns > count(buf) ){
863
aSrc = (int*)fossil_malloc(sizeof(int)*nTicketBslns);
864
}
865
for(i=0; i<nField; i++){
866
if( aField[i].zBsln && aUsed[i]==JCARD_ASSIGN ){
867
s = db_int(0,"SELECT \"%w\" FROM ticket WHERE tkt_uuid = '%q'",
868
aField[i].zBsln, zTktId );
869
if( s > 0 ) aSrc[nSrc++] = s;
870
}
871
}
872
if( nSrc ) content_deltify(rid, aSrc, nSrc, 0);
873
if( aSrc!=&(buf[0]) ) fossil_free( aSrc );
874
}
875
if( needMod ){
876
moderation_table_create();
877
db_multi_exec(
878
"INSERT INTO modreq(objid, tktid) VALUES(%d,%Q)",
879
rid, zTktId
880
);
881
}else{
882
db_add_unsent(rid);
883
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", rid);
884
}
885
result = (manifest_crosslink(rid, pTicket, MC_NONE)==0);
886
assert( blob_is_reset(pTicket) );
887
if( !result ){
888
result = manifest_crosslink_end(MC_PERMIT_HOOKS);
889
}else{
890
manifest_crosslink_end(MC_NONE);
891
}
892
return result;
893
}
894
895
/*
896
** Subscript command: submit_ticket
897
**
898
** Construct and submit a new ticket artifact. The fields of the artifact
899
** are the names of the columns in the TICKET table. The content is
900
** taken from TH variables. If the content is unchanged, the field is
901
** omitted from the artifact. Fields whose names begin with "private_"
902
** are concealed using the db_conceal() function.
903
*/
904
static int submitTicketCmd(
905
Th_Interp *interp,
906
void *pUuid,
907
int argc,
908
const char **argv,
909
int *argl
910
){
911
char *zDate, *aUsed;
912
const char *zUuid;
913
int i;
914
int nJ = 0, rc = TH_OK;
915
Blob tktchng, cksum;
916
int needMod;
917
918
if( !cgi_csrf_safe(2) ){
919
@ <p class="generalError">Error: Invalid CSRF token.</p>
920
return TH_OK;
921
}
922
if( !captcha_is_correct(0) ){
923
@ <p class="generalError">Error: Incorrect security code.</p>
924
return TH_OK;
925
}
926
zUuid = (const char *)pUuid;
927
blob_zero(&tktchng);
928
zDate = date_in_standard_format("now");
929
blob_appendf(&tktchng, "D %s\n", zDate);
930
free(zDate);
931
aUsed = fossil_malloc_zero( nField );
932
for(i=0; i<nField; i++){
933
if( aField[i].zAppend ){
934
blob_appendf(&tktchng, "J +%s %z\n", aField[i].zName,
935
fossilize(aField[i].zAppend, -1));
936
++nJ;
937
aUsed[i] = JCARD_APPEND;
938
}
939
}
940
for(i=0; i<nField; i++){
941
const char *zValue;
942
int nValue;
943
if( aField[i].zAppend ) continue;
944
zValue = Th_Fetch(aField[i].zName, &nValue);
945
if( zValue ){
946
nValue = TH1_LEN(nValue);
947
while( nValue>0 && fossil_isspace(zValue[nValue-1]) ){ nValue--; }
948
if( ((aField[i].mUsed & USEDBY_TICKETCHNG)!=0 && nValue>0)
949
|| memcmp(zValue, aField[i].zValue, nValue)!=0
950
||(int)strlen(aField[i].zValue)!=nValue
951
){
952
if( memcmp(aField[i].zName, "private_", 8)==0 ){
953
zValue = db_conceal(zValue, nValue);
954
blob_appendf(&tktchng, "J %s %s\n", aField[i].zName, zValue);
955
aUsed[i] = JCARD_PRIVATE;
956
}else{
957
blob_appendf(&tktchng, "J %s %#F\n", aField[i].zName, nValue, zValue);
958
aUsed[i] = JCARD_ASSIGN;
959
}
960
nJ++;
961
}
962
}
963
}
964
if( *(char**)pUuid ){
965
zUuid = db_text(0,
966
"SELECT tkt_uuid FROM ticket WHERE tkt_uuid GLOB '%q*'", P("name")
967
);
968
}else{
969
zUuid = db_text(0, "SELECT lower(hex(randomblob(20)))");
970
}
971
*(const char**)pUuid = zUuid;
972
blob_appendf(&tktchng, "K %s\n", zUuid);
973
blob_appendf(&tktchng, "U %F\n", login_name());
974
md5sum_blob(&tktchng, &cksum);
975
blob_appendf(&tktchng, "Z %b\n", &cksum);
976
if( nJ==0 ){
977
blob_reset(&tktchng);
978
goto finish;
979
}
980
needMod = ticket_need_moderation(0);
981
if( g.zPath[0]=='d' ){
982
const char *zNeedMod = needMod ? "required" : "skipped";
983
/* If called from /debug_tktnew or /debug_tktedit... */
984
@ <div style="color:blue">
985
@ <p>Ticket artifact that would have been submitted:</p>
986
@ <blockquote><pre>%h(blob_str(&tktchng))</pre></blockquote>
987
@ <blockquote><pre>Moderation would be %h(zNeedMod).</pre></blockquote>
988
@ </div>
989
@ <hr>
990
}else{
991
if( g.thTrace ){
992
Th_Trace("submit_ticket {\n<blockquote><pre>\n%h\n</pre></blockquote>\n"
993
"}<br>\n",
994
blob_str(&tktchng));
995
}
996
ticket_put(&tktchng, zUuid, aUsed, needMod);
997
rc = ticket_change(zUuid);
998
}
999
finish:
1000
fossil_free( aUsed );
1001
return rc;
1002
}
1003
1004
1005
/*
1006
** WEBPAGE: tktnew
1007
** WEBPAGE: debug_tktnew
1008
**
1009
** Enter a new ticket. The tktnew_template script in the ticket
1010
** configuration is used. The /tktnew page is the official ticket
1011
** entry page. The /debug_tktnew page is used for debugging the
1012
** tktnew_template in the ticket configuration. /debug_tktnew works
1013
** just like /tktnew except that it does not really save the new ticket
1014
** when you press submit - it just prints the ticket artifact at the
1015
** top of the screen.
1016
*/
1017
void tktnew_page(void){
1018
const char *zScript;
1019
char *zNewUuid = 0;
1020
int uid;
1021
1022
login_check_credentials();
1023
if( !g.perm.NewTkt ){ login_needed(g.anon.NewTkt); return; }
1024
if( P("cancel") ){
1025
cgi_redirect("home");
1026
}
1027
style_set_current_feature("tkt");
1028
style_header("New Ticket");
1029
ticket_standard_submenu(T_ALL_BUT(T_NEW));
1030
if( g.thTrace ) Th_Trace("BEGIN_TKTNEW<br>\n", -1);
1031
ticket_init();
1032
initializeVariablesFromCGI();
1033
getAllTicketFields();
1034
initializeVariablesFromDb();
1035
if( g.zPath[0]=='d' ) showAllFields();
1036
form_begin(0, "%R/%s", g.zPath);
1037
if( P("date_override") && g.perm.Setup ){
1038
@ <input type="hidden" name="date_override" value="%h(P("date_override"))">
1039
}
1040
zScript = ticket_newpage_code();
1041
Th_Store("private_contact", "");
1042
if( g.zLogin && g.zLogin[0] ){
1043
uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", g.zLogin);
1044
if( uid ){
1045
char * zEmail =
1046
db_text(0, "SELECT find_emailaddr(info) FROM user WHERE uid=%d",
1047
uid);
1048
if( zEmail ){
1049
Th_StoreUnsafe("private_contact", zEmail);
1050
fossil_free(zEmail);
1051
}
1052
}
1053
}
1054
Th_StoreUnsafe("login", login_name());
1055
Th_Store("date", db_text(0, "SELECT datetime('now')"));
1056
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd,
1057
(void*)&zNewUuid, 0);
1058
if( g.thTrace ) Th_Trace("BEGIN_TKTNEW_SCRIPT<br>\n", -1);
1059
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zNewUuid ){
1060
if( P("submitandnew") ){
1061
cgi_redirect(mprintf("%R/tktnew/%s", zNewUuid));
1062
}else{
1063
cgi_redirect(mprintf("%R/tktview/%s", zNewUuid));
1064
}
1065
return;
1066
}
1067
captcha_generate(0);
1068
@ </form>
1069
if( g.thTrace ) Th_Trace("END_TKTVIEW<br>\n", -1);
1070
style_finish_page();
1071
}
1072
1073
/*
1074
** WEBPAGE: tktedit
1075
** WEBPAGE: debug_tktedit
1076
**
1077
** Edit a ticket. The ticket is identified by the name CGI parameter.
1078
** /tktedit is the official page. The /debug_tktedit page does the same
1079
** thing except that it does not save the ticket change record when you
1080
** press submit - it instead prints the ticket change record at the top
1081
** of the page. The /debug_tktedit page is intended to be used when
1082
** debugging ticket configurations.
1083
*/
1084
void tktedit_page(void){
1085
const char *zScript;
1086
int nName;
1087
const char *zName;
1088
int nRec;
1089
1090
login_check_credentials();
1091
if( !g.perm.ApndTkt && !g.perm.WrTkt ){
1092
login_needed(g.anon.ApndTkt || g.anon.WrTkt);
1093
return;
1094
}
1095
zName = P("name");
1096
if( P("cancel") ){
1097
cgi_redirectf("tktview/%T", zName);
1098
}
1099
style_set_current_feature("tkt");
1100
style_header("Edit Ticket");
1101
if( zName==0 || (nName = strlen(zName))<4 || nName>HNAME_LEN_SHA1
1102
|| !validate16(zName,nName) ){
1103
@ <span class="tktError">Not a valid ticket id: "%h(zName)"</span>
1104
style_finish_page();
1105
return;
1106
}
1107
nRec = db_int(0, "SELECT count(*) FROM ticket WHERE tkt_uuid GLOB '%q*'",
1108
zName);
1109
if( nRec==0 ){
1110
@ <span class="tktError">No such ticket: "%h(zName)"</span>
1111
style_finish_page();
1112
return;
1113
}
1114
if( nRec>1 ){
1115
@ <span class="tktError">%d(nRec) tickets begin with:
1116
@ "%h(zName)"</span>
1117
style_finish_page();
1118
return;
1119
}
1120
if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br>\n", -1);
1121
ticket_init();
1122
getAllTicketFields();
1123
initializeVariablesFromCGI();
1124
initializeVariablesFromDb();
1125
if( g.zPath[0]=='d' ) showAllFields();
1126
form_begin(0, "%R/%s", g.zPath);
1127
@ <input type="hidden" name="name" value="%s(zName)">
1128
zScript = ticket_editpage_code();
1129
Th_StoreUnsafe("login", login_name());
1130
Th_Store("date", db_text(0, "SELECT datetime('now')"));
1131
Th_CreateCommand(g.interp, "append_field", appendRemarkCmd, 0, 0);
1132
Th_CreateCommand(g.interp, "submit_ticket", submitTicketCmd, (void*)&zName,0);
1133
if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT_SCRIPT<br>\n", -1);
1134
if( Th_Render(zScript)==TH_RETURN && !g.thTrace && zName ){
1135
cgi_redirect(mprintf("%R/tktview/%s", zName));
1136
return;
1137
}
1138
captcha_generate(0);
1139
@ </form>
1140
if( g.thTrace ) Th_Trace("BEGIN_TKTEDIT<br>\n", -1);
1141
style_finish_page();
1142
}
1143
1144
/*
1145
** Check the ticket table schema in zSchema to see if it appears to
1146
** be well-formed. If everything is OK, return NULL. If something is
1147
** amiss, then return a pointer to a string (obtained from malloc) that
1148
** describes the problem.
1149
*/
1150
char *ticket_schema_check(const char *zSchema){
1151
char *zErr = 0;
1152
int rc;
1153
sqlite3 *db;
1154
rc = sqlite3_open(":memory:", &db);
1155
if( rc==SQLITE_OK ){
1156
rc = sqlite3_exec(db, zSchema, 0, 0, &zErr);
1157
if( rc!=SQLITE_OK ){
1158
sqlite3_close(db);
1159
return zErr;
1160
}
1161
rc = sqlite3_exec(db, "SELECT tkt_id, tkt_uuid, tkt_mtime FROM ticket",
1162
0, 0, 0);
1163
if( rc!=SQLITE_OK ){
1164
zErr = mprintf("schema fails to define valid a TICKET "
1165
"table containing all required fields");
1166
}else{
1167
rc = sqlite3_exec(db, "SELECT tkt_id, tkt_mtime FROM ticketchng", 0,0,0);
1168
if( rc!=SQLITE_OK ){
1169
zErr = mprintf("schema fails to define valid a TICKETCHNG "
1170
"table containing all required fields");
1171
}
1172
}
1173
sqlite3_close(db);
1174
}
1175
return zErr;
1176
}
1177
1178
/*
1179
** Draw a timeline for a ticket with tag.tagid given by the tagid
1180
** parameter.
1181
**
1182
** If zType[0]=='c' then only show check-ins associated with the
1183
** ticket. For any other value of zType, show all events associated
1184
** with the ticket.
1185
*/
1186
void tkt_draw_timeline(int tagid, const char *zType){
1187
Stmt q;
1188
char *zFullUuid;
1189
char *zSQL;
1190
zFullUuid = db_text(0, "SELECT substr(tagname, 5) FROM tag WHERE tagid=%d",
1191
tagid);
1192
if( zType[0]=='c' ){
1193
zSQL = mprintf(
1194
"%s AND event.objid IN "
1195
" (SELECT srcid FROM backlink WHERE target GLOB '%.4s*' "
1196
"AND srctype=0 "
1197
"AND '%s' GLOB (target||'*')) "
1198
"ORDER BY mtime DESC",
1199
timeline_query_for_www(), zFullUuid, zFullUuid
1200
);
1201
}else{
1202
zSQL = mprintf(
1203
"%s AND event.objid IN "
1204
" (SELECT rid FROM tagxref WHERE tagid=%d"
1205
" UNION"
1206
" SELECT CASE srctype WHEN 2 THEN"
1207
" (SELECT rid FROM tagxref WHERE tagid=backlink.srcid"
1208
" ORDER BY mtime DESC LIMIT 1)"
1209
" ELSE srcid END"
1210
" FROM backlink"
1211
" WHERE target GLOB '%.4s*'"
1212
" AND '%s' GLOB (target||'*')"
1213
" UNION SELECT attachid FROM attachment"
1214
" WHERE target=%Q) "
1215
"ORDER BY mtime DESC",
1216
timeline_query_for_www(), tagid, zFullUuid, zFullUuid, zFullUuid
1217
);
1218
}
1219
db_prepare(&q, "%z", zSQL/*safe-for-%s*/);
1220
www_print_timeline(&q,
1221
TIMELINE_ARTID | TIMELINE_DISJOINT | TIMELINE_GRAPH | TIMELINE_NOTKT |
1222
TIMELINE_REFS,
1223
0, 0, 0, 0, 0, 0);
1224
db_finalize(&q);
1225
fossil_free(zFullUuid);
1226
}
1227
1228
/*
1229
** WEBPAGE: tkttimeline
1230
** URL: /tkttimeline/TICKETUUID
1231
**
1232
** Show the change history for a single ticket in timeline format.
1233
**
1234
** Query parameters:
1235
**
1236
** y=ci Show only check-ins associated with the ticket
1237
*/
1238
void tkttimeline_page(void){
1239
char *zTitle;
1240
const char *zUuid;
1241
int tagid;
1242
char zGlobPattern[50];
1243
const char *zType;
1244
1245
login_check_credentials();
1246
if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1247
login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1248
return;
1249
}
1250
zUuid = PD("name","");
1251
zType = PD("y","a");
1252
if( zType[0]!='c' ){
1253
if( g.perm.Read ){
1254
style_submenu_element("Check-ins", "%R/tkttimeline/%T?y=ci", zUuid);
1255
}
1256
}else{
1257
style_submenu_element("Timeline", "%R/tkttimeline/%T", zUuid);
1258
}
1259
style_submenu_element("History", "%R/tkthistory/%s", zUuid);
1260
style_submenu_element("Status", "%R/info/%s", zUuid);
1261
if( zType[0]=='c' ){
1262
zTitle = mprintf("Check-ins Associated With Ticket %h", zUuid);
1263
}else{
1264
zTitle = mprintf("Timeline Of Ticket %h", zUuid);
1265
}
1266
style_set_current_feature("tkt");
1267
style_header("%z", zTitle);
1268
1269
sqlite3_snprintf(6, zGlobPattern, "%s", zUuid);
1270
canonical16(zGlobPattern, strlen(zGlobPattern));
1271
tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
1272
if( tagid==0 ){
1273
@ No such ticket: %h(zUuid)
1274
style_finish_page();
1275
return;
1276
}
1277
tkt_draw_timeline(tagid, zType);
1278
style_finish_page();
1279
}
1280
1281
/*
1282
** WEBPAGE: tkthistory
1283
** URL: /tkthistory/TICKETUUID
1284
**
1285
** Show the complete change history for a single ticket. Or (to put it
1286
** another way) show a list of artifacts associated with a single ticket.
1287
**
1288
** By default, the artifacts are decoded and formatted. Text fields
1289
** are formatted as text/plain, since in the general case Fossil does
1290
** not have knowledge of the encoding. If the "raw" query parameter
1291
** is present, then the undecoded and unformatted text of each artifact
1292
** is displayed.
1293
**
1294
** Reassignments of a field of the TICKET table that has a corresponding
1295
** "baseline for ..." companion are rendered as unified diffs.
1296
*/
1297
void tkthistory_page(void){
1298
Stmt q;
1299
char *zTitle;
1300
const char *zUuid;
1301
int tagid;
1302
int nChng = 0;
1303
Blob *aLastVal = 0; /* holds the last rendered value for each field */
1304
1305
login_check_credentials();
1306
if( !g.perm.Hyperlink || !g.perm.RdTkt ){
1307
login_needed(g.anon.Hyperlink && g.anon.RdTkt);
1308
return;
1309
}
1310
zUuid = PD("name","");
1311
zTitle = mprintf("History Of Ticket %h", zUuid);
1312
style_submenu_element("Status", "%R/info/%s", zUuid);
1313
if( g.perm.Read ){
1314
style_submenu_element("Check-ins", "%R/tkttimeline/%s?y=ci", zUuid);
1315
}
1316
style_submenu_element("Timeline", "%R/tkttimeline/%s", zUuid);
1317
if( P("raw")!=0 ){
1318
style_submenu_element("Decoded", "%R/tkthistory/%s", zUuid);
1319
}else if( g.perm.Admin ){
1320
style_submenu_element("Raw", "%R/tkthistory/%s?raw", zUuid);
1321
}
1322
style_set_current_feature("tkt");
1323
style_header("%z", zTitle);
1324
1325
tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",zUuid);
1326
if( tagid==0 ){
1327
@ No such ticket: %h(zUuid)
1328
style_finish_page();
1329
return;
1330
}
1331
if( P("raw")!=0 ){
1332
@ <h2>Raw Artifacts Associated With Ticket %h(zUuid)</h2>
1333
}else{
1334
@ <h2>Artifacts Associated With Ticket %h(zUuid)</h2>
1335
getAllTicketFields();
1336
if( nTicketBslns ){
1337
aLastVal = blobarray_new(nField);
1338
}
1339
}
1340
db_prepare(&q,
1341
"SELECT datetime(mtime,toLocal()), objid, uuid, NULL, NULL, NULL"
1342
" FROM event, blob"
1343
" WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
1344
" AND blob.rid=event.objid"
1345
" UNION "
1346
"SELECT datetime(mtime,toLocal()), attachid, uuid, src, filename, user"
1347
" FROM attachment, blob"
1348
" WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
1349
" AND blob.rid=attachid"
1350
" ORDER BY 1",
1351
tagid, tagid
1352
);
1353
for(nChng=0; db_step(&q)==SQLITE_ROW; nChng++){
1354
Manifest *pTicket;
1355
const char *zDate = db_column_text(&q, 0);
1356
int rid = db_column_int(&q, 1);
1357
const char *zChngUuid = db_column_text(&q, 2);
1358
const char *zFile = db_column_text(&q, 4);
1359
if( nChng==0 ){
1360
@ <ol class="tkt-changes">
1361
}
1362
if( zFile!=0 ){
1363
const char *zSrc = db_column_text(&q, 3);
1364
const char *zUser = db_column_text(&q, 5);
1365
@
1366
@ <li id="%S(zChngUuid)"><p><span>
1367
if( zSrc==0 || zSrc[0]==0 ){
1368
@ Delete attachment "%h(zFile)"
1369
}else{
1370
@ Add attachment
1371
@ "%z(href("%R/artifact/%!S",zSrc))%s(zFile)</a>"
1372
}
1373
@ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
1374
@ (rid %d(rid)) by
1375
hyperlink_to_user(zUser,zDate," on");
1376
hyperlink_to_date(zDate, ".</p>");
1377
}else{
1378
pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1379
if( pTicket ){
1380
@
1381
@ <li id="%S(zChngUuid)"><p><span>Ticket change
1382
@ [%z(href("%R/artifact/%!S",zChngUuid))%S(zChngUuid)</a>]</span>
1383
@ (rid %d(rid)) by
1384
hyperlink_to_user(pTicket->zUser,zDate," on");
1385
hyperlink_to_date(zDate, ":");
1386
@ </p>
1387
if( P("raw")!=0 ){
1388
Blob c;
1389
content_get(rid, &c);
1390
@ <blockquote><pre>
1391
@ %h(blob_str(&c))
1392
@ </pre></blockquote>
1393
blob_reset(&c);
1394
}else{
1395
ticket_output_change_artifact(pTicket, "a", nChng, aLastVal);
1396
}
1397
}
1398
manifest_destroy(pTicket);
1399
}
1400
@ </li>
1401
}
1402
db_finalize(&q);
1403
if( nChng ){
1404
@ </ol>
1405
}
1406
style_finish_page();
1407
if( aLastVal ) blobarray_delete(aLastVal, nField);
1408
}
1409
1410
/*
1411
** Return TRUE if the given BLOB contains a newline character.
1412
*/
1413
static int contains_newline(Blob *p){
1414
const char *z = blob_str(p);
1415
while( *z ){
1416
if( *z=='\n' ) return 1;
1417
z++;
1418
}
1419
return 0;
1420
}
1421
1422
/*
1423
** The pTkt object is a ticket change artifact. Output a detailed
1424
** description of this object.
1425
**
1426
** If `aLastVal` is not NULL then render selected fields as unified diffs
1427
** and update corresponding elements of that array with values from `pTkt`.
1428
*/
1429
void ticket_output_change_artifact(
1430
Manifest *pTkt, /* Parsed artifact for the ticket change */
1431
const char *zListType, /* Which type of list */
1432
int n, /* Which ticket change is this */
1433
Blob *aLastVal /* Array of the latest values for the diffs */
1434
){
1435
int i;
1436
if( zListType==0 ) zListType = "1";
1437
getAllTicketFields();
1438
@ <ol type="%s(zListType)">
1439
for(i=0; i<pTkt->nField; i++){
1440
const char *z = pTkt->aField[i].zName;
1441
const char *zX = z[0]=='+' ? z+1 : z;
1442
const int id = fieldId(zX);
1443
const char *zValue = pTkt->aField[i].zValue;
1444
const size_t nValue = strlen(zValue);
1445
const int bLong = nValue>50 || memchr(zValue,'\n',nValue)!=NULL;
1446
/* zValue is long enough to justify a <blockquote> */
1447
const int bCanDiff = aLastVal && id>=0 && aField[id].zBsln;
1448
/* preliminary flag for rendering via unified diff */
1449
int bAppend = 0; /* zValue is being appended to a TICKET's field */
1450
int bRegular = 0; /* prev value of a TICKET's field is being superseded*/
1451
@ <li>\
1452
if( id<0 ){
1453
@ Untracked field %h(zX):
1454
}else if( aField[id].mUsed==USEDBY_TICKETCHNG ){
1455
@ %h(zX):
1456
}else if( n==0 ){
1457
@ %h(zX) initialized to:
1458
}else if( z[0]=='+' && (aField[id].mUsed&USEDBY_TICKET)!=0 ){
1459
@ Appended to %h(zX):
1460
bAppend = 1;
1461
}else{
1462
if( !bCanDiff ){
1463
@ %h(zX) changed to: \
1464
}
1465
bRegular = 1;
1466
}
1467
if( bCanDiff ){
1468
Blob *prev = aLastVal+id;
1469
Blob val = BLOB_INITIALIZER;
1470
if( nValue ){
1471
blob_init(&val, zValue, nValue+1);
1472
val.nUsed--; /* makes blob_str() faster */
1473
}
1474
if( bRegular && nValue && blob_buffer(prev) && blob_size(prev) ){
1475
Blob d = BLOB_INITIALIZER;
1476
DiffConfig DCfg;
1477
construct_diff_flags(1, &DCfg);
1478
DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1479
text_diff(prev, &val, &d, &DCfg);
1480
@ %h(zX) changed as:
1481
@ %s(blob_str(&d))
1482
@ </li>
1483
blob_reset(&d);
1484
}else{
1485
if( bRegular ){
1486
@ %h(zX) changed to:
1487
}
1488
if( bLong ){
1489
@ <blockquote><pre class='verbatim'>
1490
@ %h(zValue)
1491
@ </pre></blockquote></li>
1492
}else{
1493
@ "%h(zValue)"</li>
1494
}
1495
}
1496
if( blob_buffer(prev) && blob_size(prev) && !bAppend ){
1497
blob_truncate(prev,0);
1498
}
1499
if( nValue ) blob_appendb(prev, &val);
1500
blob_reset(&val);
1501
}else{
1502
if( bLong ){
1503
@ <blockquote><pre class='verbatim'>
1504
@ %h(zValue)
1505
@ </pre></blockquote></li>
1506
}else{
1507
@ "%h(zValue)"</li>
1508
}
1509
}
1510
}
1511
@ </ol>
1512
}
1513
1514
/*
1515
** COMMAND: ticket*
1516
**
1517
** Usage: %fossil ticket SUBCOMMAND ...
1518
**
1519
** Run various subcommands to control tickets
1520
**
1521
** > fossil ticket show (REPORTTITLE|REPORTNR) ?TICKETFILTER? ?OPTIONS?
1522
**
1523
** Options:
1524
** -l|--limit LIMITCHAR
1525
** --quote
1526
** -R|--repository REPO
1527
**
1528
** Run the ticket report, identified by the report format title
1529
** used in the GUI. The data is written as flat file on stdout,
1530
** using TAB as separator. The separator can be changed using
1531
** the -l or --limit option.
1532
**
1533
** If TICKETFILTER is given on the command line, the query is
1534
** limited with a new WHERE-condition.
1535
** example: Report lists a column # with the uuid
1536
** TICKETFILTER may be [#]='uuuuuuuuu'
1537
** example: Report only lists rows with status not open
1538
** TICKETFILTER: status != 'open'
1539
**
1540
** If --quote is used, the tickets are encoded by quoting special
1541
** chars (space -> \\s, tab -> \\t, newline -> \\n, cr -> \\r,
1542
** formfeed -> \\f, vtab -> \\v, nul -> \\0, \\ -> \\\\).
1543
** Otherwise, the simplified encoding as on the show report raw page
1544
** in the GUI is used. This has no effect in JSON mode.
1545
**
1546
** Instead of the report title it's possible to use the report
1547
** number; the special report number 0 lists all columns defined in
1548
** the ticket table.
1549
**
1550
** > fossil ticket list fields
1551
** > fossil ticket ls fields
1552
**
1553
** List all fields defined for ticket in the fossil repository.
1554
**
1555
** > fossil ticket list reports
1556
** > fossil ticket ls reports
1557
**
1558
** List all ticket reports defined in the fossil repository.
1559
**
1560
** > fossil ticket set TICKETUUID (FIELD VALUE)+ ?--quote?
1561
** > fossil ticket change TICKETUUID (FIELD VALUE)+ ?--quote?
1562
**
1563
** Change ticket identified by TICKETUUID to set the values of
1564
** each field FIELD to VALUE.
1565
**
1566
** Field names as defined in the TICKET table. By default, these
1567
** names include: type, status, subsystem, priority, severity, foundin,
1568
** resolution, title, and comment, but other field names can be added
1569
** or substituted in customized installations.
1570
**
1571
** If you use +FIELD, the VALUE is appended to the field FIELD. You
1572
** can use more than one field/value pair on the command line. Using
1573
** --quote enables the special character decoding as in "ticket
1574
** show", which allows setting multiline text or text with special
1575
** characters.
1576
**
1577
** > fossil ticket add FIELD VALUE ?FIELD VALUE .. ? ?--quote?
1578
**
1579
** Like set, but create a new ticket with the given values.
1580
**
1581
** > fossil ticket history TICKETUUID
1582
**
1583
** Show the complete change history for the ticket
1584
**
1585
** Note that the values in set|add are not validated against the
1586
** definitions given in "Ticket Common Script".
1587
*/
1588
void ticket_cmd(void){
1589
int n;
1590
const char *zUser;
1591
const char *zDate;
1592
const char *zTktUuid;
1593
1594
/* do some ints, we want to be inside a check-out */
1595
db_find_and_open_repository(0, 0);
1596
user_select();
1597
1598
zUser = find_option("user-override",0,1);
1599
if( zUser==0 ) zUser = login_name();
1600
zDate = find_option("date-override",0,1);
1601
if( zDate==0 ) zDate = "now";
1602
zDate = date_in_standard_format(zDate);
1603
zTktUuid = find_option("uuid-override",0,1);
1604
if( zTktUuid && (strlen(zTktUuid)!=40 || !validate16(zTktUuid,40)) ){
1605
fossil_fatal("invalid --uuid-override: must be 40 characters of hex");
1606
}
1607
1608
/*
1609
** Check that the user exists.
1610
*/
1611
if( !db_exists("SELECT 1 FROM user WHERE login=%Q", zUser) ){
1612
fossil_fatal("no such user: %s", zUser);
1613
}
1614
1615
if( g.argc<3 ){
1616
usage("add|change|list|set|show|history");
1617
}
1618
n = strlen(g.argv[2]);
1619
if( n==1 && g.argv[2][0]=='s' ){
1620
/* set/show cannot be distinguished, so show the usage */
1621
usage("add|change|list|set|show|history");
1622
}
1623
if(( strncmp(g.argv[2],"list",n)==0 ) || ( strncmp(g.argv[2],"ls",n)==0 )){
1624
if( g.argc==3 ){
1625
usage("list fields|reports");
1626
}else{
1627
n = strlen(g.argv[3]);
1628
if( !strncmp(g.argv[3],"fields",n) ){
1629
/* simply show all field names */
1630
int i;
1631
1632
/* read all available ticket fields */
1633
getAllTicketFields();
1634
for(i=0; i<nField; i++){
1635
printf("%s\n",aField[i].zName);
1636
}
1637
}else if( !strncmp(g.argv[3],"reports",n) ){
1638
rpt_list_reports();
1639
}else{
1640
fossil_fatal("unknown ticket list option '%s'!",g.argv[3]);
1641
}
1642
}
1643
}else{
1644
/* add a new ticket or set fields on existing tickets */
1645
tTktShowEncoding tktEncoding;
1646
1647
tktEncoding = find_option("quote",0,0) ? tktFossilize : tktNoTab;
1648
1649
if( strncmp(g.argv[2],"show",n)==0 ){
1650
if( g.argc==3 ){
1651
usage("show REPORTNR");
1652
}else{
1653
const char *zRep = 0;
1654
const char *zSep = 0;
1655
const char *zFilterUuid = 0;
1656
zSep = find_option("limit","l",1);
1657
zRep = g.argv[3];
1658
if( !strcmp(zRep,"0") ){
1659
zRep = 0;
1660
}
1661
if( g.argc>4 ){
1662
zFilterUuid = g.argv[4];
1663
}
1664
rptshow( zRep, zSep, zFilterUuid, tktEncoding );
1665
}
1666
}else{
1667
/* add a new ticket or update an existing ticket */
1668
enum { set,add,history,err } eCmd = err;
1669
int i = 0;
1670
Blob tktchng, cksum;
1671
char *aUsed;
1672
1673
/* get command type (set/add) and get uuid, if needed for set */
1674
if( strncmp(g.argv[2],"set",n)==0 || strncmp(g.argv[2],"change",n)==0 ||
1675
strncmp(g.argv[2],"history",n)==0 ){
1676
if( strncmp(g.argv[2],"history",n)==0 ){
1677
eCmd = history;
1678
}else{
1679
eCmd = set;
1680
}
1681
if( g.argc==3 ){
1682
usage("set|change|history TICKETUUID");
1683
}
1684
zTktUuid = db_text(0,
1685
"SELECT tkt_uuid FROM ticket WHERE tkt_uuid GLOB '%q*'", g.argv[3]
1686
);
1687
if( !zTktUuid ){
1688
fossil_fatal("unknown ticket: '%s'!",g.argv[3]);
1689
}
1690
i=4;
1691
}else if( strncmp(g.argv[2],"add",n)==0 ){
1692
eCmd = add;
1693
i = 3;
1694
if( zTktUuid==0 ){
1695
zTktUuid = db_text(0, "SELECT lower(hex(randomblob(20)))");
1696
}
1697
}
1698
/* none of set/add, so show the usage! */
1699
if( eCmd==err ){
1700
usage("add|fieldlist|set|show|history");
1701
}
1702
1703
/* we just handle history separately here, does not get out */
1704
if( eCmd==history ){
1705
Stmt q;
1706
int tagid;
1707
1708
if( i != g.argc ){
1709
fossil_fatal("no other parameters expected to %s!",g.argv[2]);
1710
}
1711
tagid = db_int(0, "SELECT tagid FROM tag WHERE tagname GLOB 'tkt-%q*'",
1712
zTktUuid);
1713
if( tagid==0 ){
1714
fossil_fatal("no such ticket %h", zTktUuid);
1715
}
1716
db_prepare(&q,
1717
"SELECT datetime(mtime,toLocal()), objid, NULL, NULL, NULL"
1718
" FROM event, blob"
1719
" WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
1720
" AND blob.rid=event.objid"
1721
" UNION "
1722
"SELECT datetime(mtime,toLocal()), attachid, filename, "
1723
" src, user"
1724
" FROM attachment, blob"
1725
" WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
1726
" AND blob.rid=attachid"
1727
" ORDER BY 1 DESC",
1728
tagid, tagid
1729
);
1730
while( db_step(&q)==SQLITE_ROW ){
1731
Manifest *pTicket;
1732
const char *zDate = db_column_text(&q, 0);
1733
int rid = db_column_int(&q, 1);
1734
const char *zFile = db_column_text(&q, 2);
1735
if( zFile!=0 ){
1736
const char *zSrc = db_column_text(&q, 3);
1737
const char *zUser = db_column_text(&q, 4);
1738
if( zSrc==0 || zSrc[0]==0 ){
1739
fossil_print("Delete attachment %s\n", zFile);
1740
}else{
1741
fossil_print("Add attachment %s\n", zFile);
1742
}
1743
fossil_print(" by %s on %s\n", zUser, zDate);
1744
}else{
1745
pTicket = manifest_get(rid, CFTYPE_TICKET, 0);
1746
if( pTicket ){
1747
int i;
1748
1749
fossil_print("Ticket Change by %s on %s:\n",
1750
pTicket->zUser, zDate);
1751
for(i=0; i<pTicket->nField; i++){
1752
Blob val;
1753
const char *z;
1754
z = pTicket->aField[i].zName;
1755
blob_set(&val, pTicket->aField[i].zValue);
1756
if( z[0]=='+' ){
1757
fossil_print(" Append to ");
1758
z++;
1759
}else{
1760
fossil_print(" Change ");
1761
}
1762
fossil_print("%h: ",z);
1763
if( blob_size(&val)>50 || contains_newline(&val)) {
1764
fossil_print("\n ");
1765
comment_print(blob_str(&val),0,4,-1,get_comment_format());
1766
}else{
1767
fossil_print("%s\n",blob_str(&val));
1768
}
1769
blob_reset(&val);
1770
}
1771
}
1772
manifest_destroy(pTicket);
1773
}
1774
}
1775
db_finalize(&q);
1776
return;
1777
}
1778
/* read all given ticket field/value pairs from command line */
1779
if( i==g.argc ){
1780
fossil_fatal("empty %s command aborted!",g.argv[2]);
1781
}
1782
getAllTicketFields();
1783
/* read command-line and assign fields in the aField[].zValue array */
1784
while( i<g.argc ){
1785
char *zFName;
1786
char *zFValue;
1787
int j;
1788
int append = 0;
1789
1790
zFName = g.argv[i++];
1791
if( i==g.argc ){
1792
fossil_fatal("missing value for '%s'!",zFName);
1793
}
1794
zFValue = g.argv[i++];
1795
if( tktEncoding == tktFossilize ){
1796
zFValue=fossil_strdup(zFValue);
1797
defossilize(zFValue);
1798
}
1799
append = (zFName[0] == '+');
1800
if( append ){
1801
zFName++;
1802
}
1803
j = fieldId(zFName);
1804
if( j == -1 ){
1805
fossil_fatal("unknown field name '%s'!",zFName);
1806
}else{
1807
if( append ){
1808
aField[j].zAppend = zFValue;
1809
}else{
1810
aField[j].zValue = zFValue;
1811
}
1812
}
1813
}
1814
aUsed = fossil_malloc_zero( nField );
1815
1816
/* now add the needed artifacts to the repository */
1817
blob_zero(&tktchng);
1818
/* add the time to the ticket manifest */
1819
blob_appendf(&tktchng, "D %s\n", zDate);
1820
/* append defined elements */
1821
for(i=0; i<nField; i++){
1822
char *zValue = 0;
1823
char *zPfx;
1824
1825
if( aField[i].zAppend && aField[i].zAppend[0] ){
1826
zPfx = " +";
1827
zValue = aField[i].zAppend;
1828
aUsed[i] = JCARD_APPEND;
1829
}else if( aField[i].zValue && aField[i].zValue[0] ){
1830
zPfx = " ";
1831
zValue = aField[i].zValue;
1832
aUsed[i] = JCARD_ASSIGN;
1833
}else{
1834
continue;
1835
}
1836
if( memcmp(aField[i].zName, "private_", 8)==0 ){
1837
zValue = db_conceal(zValue, strlen(zValue));
1838
blob_appendf(&tktchng, "J%s%s %s\n", zPfx, aField[i].zName, zValue);
1839
aUsed[i] = JCARD_PRIVATE;
1840
}else{
1841
blob_appendf(&tktchng, "J%s%s %#F\n", zPfx,
1842
aField[i].zName, strlen(zValue), zValue);
1843
}
1844
}
1845
blob_appendf(&tktchng, "K %s\n", zTktUuid);
1846
blob_appendf(&tktchng, "U %F\n", zUser);
1847
md5sum_blob(&tktchng, &cksum);
1848
blob_appendf(&tktchng, "Z %b\n", &cksum);
1849
if( ticket_put(&tktchng, zTktUuid, aUsed,
1850
ticket_need_moderation(1) )==0 ){
1851
fossil_fatal("%s", g.zErrMsg);
1852
}else{
1853
fossil_print("ticket %s succeeded for %s\n",
1854
(eCmd==set?"set":"add"),zTktUuid);
1855
}
1856
fossil_free( aUsed );
1857
}
1858
}
1859
}
1860
1861
1862
#if INTERFACE
1863
/* Standard submenu items for wiki pages */
1864
#define T_SRCH 0x00001
1865
#define T_REPLIST 0x00002
1866
#define T_NEW 0x00004
1867
#define T_ALL 0x00007
1868
#define T_ALL_BUT(x) (T_ALL&~(x))
1869
#endif
1870
1871
/*
1872
** Add some standard submenu elements for ticket screens.
1873
*/
1874
void ticket_standard_submenu(unsigned int ok){
1875
if( (ok & T_SRCH)!=0 && search_restrict(SRCH_TKT)!=0 ){
1876
style_submenu_element("Search", "%R/tktsrch");
1877
}
1878
if( (ok & T_REPLIST)!=0 ){
1879
style_submenu_element("Reports", "%R/reportlist");
1880
}
1881
if( (ok & T_NEW)!=0 && g.anon.NewTkt ){
1882
style_submenu_element("New", "%R/tktnew");
1883
}
1884
}
1885
1886
/*
1887
** WEBPAGE: ticket
1888
**
1889
** This is intended to be the primary "Ticket" page. Render as
1890
** either ticket-search (if search is enabled) or as the
1891
** /reportlist page (if ticket search is disabled).
1892
*/
1893
void tkt_home_page(void){
1894
login_check_credentials();
1895
if( search_restrict(SRCH_TKT)!=0 ){
1896
tkt_srchpage();
1897
}else{
1898
view_list();
1899
}
1900
}
1901
1902
/*
1903
** WEBPAGE: tktsrch
1904
** Usage: /tktsrch?s=PATTERN
1905
**
1906
** Full-text search of all current tickets
1907
*/
1908
void tkt_srchpage(void){
1909
char *defaultReport;
1910
login_check_credentials();
1911
style_set_current_feature("tkt");
1912
style_header("Ticket Search");
1913
ticket_standard_submenu(T_ALL_BUT(T_SRCH));
1914
if( !search_screen(SRCH_TKT, 0) ){
1915
defaultReport = db_get("ticket-default-report", 0);
1916
if( defaultReport ){
1917
rptview_page_content(defaultReport, 0, 0);
1918
}
1919
}
1920
style_finish_page();
1921
}
1922

Keyboard Shortcuts

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