Fossil SCM

fossil-scm / src / event.c
Blame History Raw 614 lines
1
/*
2
** Copyright (c) 2010 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 to do formatting of event messages:
19
**
20
** Technical Notes
21
** Milestones
22
** Blog posts
23
** New articles
24
** Process checkpoints
25
** Announcements
26
**
27
** Do not confuse "event" artifacts with the "event" table in the
28
** repository database. An "event" artifact is a technical-note: a
29
** wiki- or blog-like essay that appears on the timeline. The "event"
30
** table records all entries on the timeline, including tech-notes.
31
**
32
** (2015-02-14): Changing the name to "tech-note" most everywhere.
33
*/
34
#include "config.h"
35
#include <assert.h>
36
#include <ctype.h>
37
#include "event.h"
38
39
/*
40
** Output a hyperlink to an technote given its tagid.
41
*/
42
void hyperlink_to_event_tagid(int tagid){
43
char *zId;
44
zId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d",
45
tagid);
46
@ [%z(href("%R/technote/%s",zId))%S(zId)</a>]
47
free(zId);
48
}
49
50
/*
51
** WEBPAGE: technote
52
** WEBPAGE: event
53
**
54
** Display a technical note (formerly called an "event").
55
**
56
** PARAMETERS:
57
**
58
** name=ID Identify the technical note to display. ID must be
59
** complete.
60
** aid=ARTIFACTID Which specific version of the tech-note. Optional.
61
** v=BOOLEAN Show details if TRUE. Default is FALSE. Optional.
62
**
63
** Display an existing tech-note identified by its ID, optionally at a
64
** specific version, and optionally with additional details.
65
*/
66
void event_page(void){
67
int rid = 0; /* rid of the event artifact */
68
char *zUuid; /* artifact hash corresponding to rid */
69
const char *zId; /* Event identifier */
70
const char *zVerbose; /* Value of verbose option */
71
char *zETime; /* Time of the tech-note */
72
char *zATime; /* Time the artifact was created */
73
int specRid; /* rid specified by aid= parameter */
74
int prevRid, nextRid; /* Previous or next edits of this tech-note */
75
Manifest *pTNote; /* Parsed technote artifact */
76
Blob fullbody; /* Complete content of the technote body */
77
Blob title; /* Title extracted from the technote body */
78
Blob tail; /* Event body that comes after the title */
79
Stmt q1; /* Query to search for the technote */
80
int verboseFlag; /* True to show details */
81
const char *zMimetype = 0; /* Mimetype of the document */
82
const char *zFullId; /* Full event identifier */
83
84
85
/* wiki-read privilege is needed in order to read tech-notes.
86
*/
87
login_check_credentials();
88
if( !g.perm.RdWiki ){
89
login_needed(g.anon.RdWiki);
90
return;
91
}
92
93
zId = P("name");
94
if( zId==0 ){ fossil_redirect_home(); return; }
95
zUuid = (char*)P("aid");
96
specRid = zUuid ? uuid_to_rid(zUuid, 0) : 0;
97
rid = nextRid = prevRid = 0;
98
db_prepare(&q1,
99
"SELECT rid FROM tagxref"
100
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')"
101
" ORDER BY mtime DESC",
102
zId
103
);
104
while( db_step(&q1)==SQLITE_ROW ){
105
nextRid = rid;
106
rid = db_column_int(&q1, 0);
107
if( specRid==0 || specRid==rid ){
108
if( db_step(&q1)==SQLITE_ROW ){
109
prevRid = db_column_int(&q1, 0);
110
}
111
break;
112
}
113
}
114
db_finalize(&q1);
115
style_set_current_feature("event");
116
if( rid==0 || (specRid!=0 && specRid!=rid) ){
117
style_header("No Such Tech-Note");
118
@ Cannot locate a technical note called <b>%h(zId)</b>.
119
style_finish_page();
120
return;
121
}
122
zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
123
zVerbose = P("v");
124
if( !zVerbose ){
125
zVerbose = P("verbose");
126
}
127
if( !zVerbose ){
128
zVerbose = P("detail"); /* deprecated */
129
}
130
verboseFlag = (zVerbose!=0) && !is_false(zVerbose);
131
132
/* Extract the event content.
133
*/
134
cgi_check_for_malice();
135
pTNote = manifest_get(rid, CFTYPE_EVENT, 0);
136
if( pTNote==0 ){
137
fossil_fatal("Object #%d is not a tech-note", rid);
138
}
139
zMimetype = wiki_filter_mimetypes(PD("mimetype",pTNote->zMimetype));
140
blob_init(&fullbody, pTNote->zWiki, -1);
141
blob_init(&title, 0, 0);
142
blob_init(&tail, 0, 0);
143
if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
144
if( !wiki_find_title(&fullbody, &title, &tail) ){
145
blob_appendf(&title, "Tech-note %S", zId);
146
tail = fullbody;
147
}
148
}else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
149
markdown_to_html(&fullbody, &title, &tail);
150
if( blob_size(&title)==0 ){
151
blob_appendf(&title, "Tech-note %S", zId);
152
}
153
}else{
154
blob_appendf(&title, "Tech-note %S", zId);
155
tail = fullbody;
156
}
157
style_header("%s", blob_str(&title));
158
if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){
159
style_submenu_element("Edit", "%R/technoteedit?name=%!S", zId);
160
if( g.perm.Attach ){
161
style_submenu_element("Attach",
162
"%R/attachadd?technote=%!S&from=%R/technote/%!S", zId, zId);
163
}
164
}
165
zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate);
166
style_submenu_element("Context", "%R/timeline?c=%.20s", zId);
167
if( g.perm.Hyperlink ){
168
if( verboseFlag ){
169
style_submenu_element("Plain",
170
"%R/technote?name=%!S&aid=%s&mimetype=text/plain",
171
zId, zUuid);
172
if( nextRid ){
173
char *zNext;
174
zNext = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nextRid);
175
style_submenu_element("Next", "%R/technote?name=%!S&aid=%s&v",
176
zId, zNext);
177
free(zNext);
178
}
179
if( prevRid ){
180
char *zPrev;
181
zPrev = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", prevRid);
182
style_submenu_element("Prev", "%R/technote?name=%!S&aid=%s&v",
183
zId, zPrev);
184
free(zPrev);
185
}
186
}else{
187
style_submenu_element("Detail", "%R/technote?name=%!S&aid=%s&v",
188
zId, zUuid);
189
}
190
}
191
192
if( verboseFlag && g.perm.Hyperlink ){
193
int i;
194
const char *zClr = 0;
195
Blob comment;
196
197
zATime = db_text(0, "SELECT datetime(%.17g)", pTNote->rDate);
198
@ <p>Tech-note [%z(href("%R/artifact/%!S",zUuid))%S(zUuid)</a>] at
199
@ [%z(href("%R/timeline?c=%T",zETime))%s(zETime)</a>]
200
@ entered by user <b>%h(pTNote->zUser)</b> on
201
@ [%z(href("%R/timeline?c=%T",zATime))%s(zATime)</a>]:</p>
202
@ <blockquote>
203
for(i=0; i<pTNote->nTag; i++){
204
if( fossil_strcmp(pTNote->aTag[i].zName,"+bgcolor")==0 ){
205
zClr = pTNote->aTag[i].zValue;
206
}
207
}
208
if( zClr && zClr[0]==0 ) zClr = 0;
209
if( zClr ){
210
@ <div style="background-color: %h(zClr);">
211
}else{
212
@ <div>
213
}
214
blob_init(&comment, pTNote->zComment, -1);
215
wiki_convert(&comment, 0, WIKI_INLINE);
216
blob_reset(&comment);
217
@ </div>
218
@ </blockquote><hr>
219
}
220
221
if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
222
wiki_convert(&fullbody, 0, 0);
223
}else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
224
cgi_append_content(blob_buffer(&tail), blob_size(&tail));
225
}else{
226
@ <pre>
227
@ %h(blob_str(&fullbody))
228
@ </pre>
229
}
230
zFullId = db_text(0, "SELECT SUBSTR(tagname,7)"
231
" FROM tag"
232
" WHERE tagname GLOB 'event-%q*'",
233
zId);
234
attachment_list(zFullId, "<h2>Attachments:</h2>", 1);
235
document_emit_js();
236
style_finish_page();
237
manifest_destroy(pTNote);
238
}
239
240
/*
241
** Add or update a new tech note to the repository. rid is id of
242
** the prior version of this technote, if any.
243
**
244
** returns 1 if the tech note was added or updated, 0 if the
245
** update failed making an invalid artifact
246
*/
247
int event_commit_common(
248
int rid, /* id of the prior version of the technote */
249
const char *zId, /* hash label for the technote */
250
const char *zBody, /* content of the technote */
251
char *zETime, /* timestamp for the technote */
252
const char *zMimetype, /* mimetype for the technote N-card */
253
const char *zComment, /* comment shown on the timeline */
254
const char *zTags, /* tags associated with this technote */
255
const char *zClr /* Background color */
256
){
257
Blob event;
258
char *zDate;
259
Blob cksum;
260
int nrid, n;
261
262
blob_init(&event, 0, 0);
263
db_begin_transaction();
264
while( fossil_isspace(zComment[0]) ) zComment++;
265
n = strlen(zComment);
266
while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; }
267
if( n>0 ){
268
blob_appendf(&event, "C %#F\n", n, zComment);
269
}
270
zDate = date_in_standard_format("now");
271
blob_appendf(&event, "D %s\n", zDate);
272
free(zDate);
273
274
zETime[10] = 'T';
275
blob_appendf(&event, "E %s %s\n", zETime, zId);
276
zETime[10] = ' ';
277
if( zMimetype && zMimetype[0] ){
278
blob_appendf(&event, "N %s\n", zMimetype);
279
}
280
if( rid ){
281
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
282
blob_appendf(&event, "P %s\n", zUuid);
283
free(zUuid);
284
}
285
if( zClr && zClr[0] ){
286
blob_appendf(&event, "T +bgcolor * %F\n", zClr);
287
}
288
if( zTags && zTags[0] ){
289
Blob tags, one;
290
int i, j;
291
Stmt q;
292
char *zBlob;
293
294
/* Load the tags string into a blob */
295
blob_zero(&tags);
296
blob_append(&tags, zTags, -1);
297
298
/* Collapse all sequences of whitespace and "," characters into
299
** a single space character */
300
zBlob = blob_str(&tags);
301
for(i=j=0; zBlob[i]; i++, j++){
302
if( fossil_isspace(zBlob[i]) || zBlob[i]==',' ){
303
while( fossil_isspace(zBlob[i+1]) ){ i++; }
304
zBlob[j] = ' ';
305
}else{
306
zBlob[j] = zBlob[i];
307
}
308
}
309
blob_resize(&tags, j);
310
311
/* Parse out each tag and load it into a temporary table for sorting */
312
db_multi_exec("CREATE TEMP TABLE newtags(x);");
313
while( blob_token(&tags, &one) ){
314
db_multi_exec("INSERT INTO newtags VALUES(%B)", &one);
315
}
316
blob_reset(&tags);
317
318
/* Extract the tags in sorted order and make an entry in the
319
** artifact for each. */
320
db_prepare(&q, "SELECT x FROM newtags ORDER BY x");
321
while( db_step(&q)==SQLITE_ROW ){
322
blob_appendf(&event, "T +sym-%F *\n", db_column_text(&q, 0));
323
}
324
db_finalize(&q);
325
}
326
if( !login_is_nobody() ){
327
blob_appendf(&event, "U %F\n", login_name());
328
}
329
blob_appendf(&event, "W %d\n%s\n", strlen(zBody), zBody);
330
md5sum_blob(&event, &cksum);
331
blob_appendf(&event, "Z %b\n", &cksum);
332
blob_reset(&cksum);
333
nrid = content_put(&event);
334
db_add_unsent(nrid);
335
if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){
336
db_end_transaction(1);
337
return 0;
338
}
339
assert( blob_is_reset(&event) );
340
content_deltify(rid, &nrid, 1, 0);
341
db_end_transaction(0);
342
return 1;
343
}
344
345
/*
346
** WEBPAGE: technoteedit
347
** WEBPAGE: eventedit
348
**
349
** Revise or create a technical note (formerly called an "event").
350
**
351
** Required query parameter:
352
**
353
** name=ID Hex hash ID of the technote. If omitted, a new
354
** tech-note is created.
355
**
356
** POST parameters from the "Cancel", "Preview", or "Submit" buttons:
357
**
358
** w=TEXT Complete text of the technote.
359
** t=TEXT Time of the technote on the timeline (ISO 8601)
360
** c=TEXT Timeline comment
361
** g=TEXT Tags associated with this technote
362
** mimetype=TEXT Mimetype for w= text
363
** newclr Use a background color
364
** clr=TEXT Background color to use if newclr
365
**
366
** For GET requests, when editing an existing technote newclr and clr
367
** are implied if a custom color has been set on the previous version
368
** of the technote.
369
*/
370
void eventedit_page(void){
371
char *zTag;
372
int rid = 0;
373
Blob event;
374
const char *zId;
375
int n;
376
const char *z;
377
char *zBody = (char*)P("w"); /* Text of the technote */
378
char *zETime = (char*)P("t"); /* Date this technote appears */
379
const char *zComment = P("c"); /* Timeline comment */
380
const char *zTags = P("g"); /* Tags added to this technote */
381
const char *zClrFlag = ""; /* "checked" for bg color */
382
const char *zClr; /* Name of the background color */
383
const char *zMimetype = P("mimetype"); /* Mimetype of zBody */
384
int isNew = 0;
385
386
if( zBody ){
387
zBody = fossil_strdup(zBody);
388
}
389
login_check_credentials();
390
zId = P("name");
391
if( zId==0 ){
392
zId = db_text(0, "SELECT lower(hex(randomblob(20)))");
393
isNew = 1;
394
}else{
395
int nId = strlen(zId);
396
if( !validate16(zId, nId) ){
397
fossil_redirect_home();
398
return;
399
}
400
}
401
zTag = mprintf("event-%s", zId);
402
rid = db_int(0,
403
"SELECT rid FROM tagxref"
404
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB '%q*')"
405
" ORDER BY mtime DESC", zTag
406
);
407
if( rid && strlen(zId)<HNAME_MIN ){
408
zId = db_text(0,
409
"SELECT substr(tagname,7) FROM tag WHERE tagname GLOB '%q*'",
410
zTag
411
);
412
}
413
free(zTag);
414
415
/* Need both check-in and wiki-write or wiki-create privileges in order
416
** to edit/create an event.
417
*/
418
if( !g.perm.Write || (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){
419
login_needed(g.anon.Write && (rid ? g.anon.WrWiki : g.anon.NewWiki));
420
return;
421
}
422
style_set_current_feature("event");
423
424
/* Figure out the color */
425
if( rid ){
426
zClr = db_text("", "SELECT bgcolor FROM event WHERE objid=%d", rid);
427
if( zClr && zClr[0] ){
428
const char * zRequestMethod = P("REQUEST_METHOD");
429
if(zRequestMethod && 'G'==zRequestMethod[0]){
430
/* Apply saved color by default for GET requests
431
** (e.g., an Edit menu link).
432
*/
433
zClrFlag = " checked";
434
}
435
}
436
}else{
437
zClr = "";
438
isNew = 1;
439
}
440
if( P("newclr") ){
441
zClr = PD("clr",zClr);
442
if( zClr[0] ) zClrFlag = " checked";
443
}
444
445
/* If editing an existing event, extract the key fields to use as
446
** a starting point for the edit.
447
*/
448
if( rid
449
&& (zBody==0 || zETime==0 || zComment==0 || zTags==0 || zMimetype==0)
450
){
451
Manifest *pTNote;
452
pTNote = manifest_get(rid, CFTYPE_EVENT, 0);
453
if( pTNote && pTNote->type==CFTYPE_EVENT ){
454
if( zBody==0 ) zBody = pTNote->zWiki;
455
if( zETime==0 ){
456
zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate);
457
}
458
if( zComment==0 ) zComment = pTNote->zComment;
459
if( zMimetype==0 ) zMimetype = pTNote->zMimetype;
460
}
461
if( zTags==0 ){
462
zTags = db_text(0,
463
"SELECT group_concat(substr(tagname,5),', ')"
464
" FROM tagxref, tag"
465
" WHERE tagxref.rid=%d"
466
" AND tagxref.tagid=tag.tagid"
467
" AND tag.tagname GLOB 'sym-*'",
468
rid
469
);
470
}
471
}
472
zETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", zETime);
473
if( P("submit")!=0 && (zBody!=0 && zComment!=0) && cgi_csrf_safe(2) ){
474
if ( !event_commit_common(rid, zId, zBody, zETime,
475
zMimetype, zComment, zTags,
476
zClrFlag[0] ? zClr : 0) ){
477
style_header("Error");
478
@ Internal error: Fossil tried to make an invalid artifact for
479
@ the edited technote.
480
style_finish_page();
481
return;
482
}
483
cgi_redirectf("%R/technote?name=%T", zId);
484
}
485
if( P("cancel")!=0 ){
486
cgi_redirectf("%R/technote?name=%T", zId);
487
return;
488
}
489
if( zBody==0 ){
490
zBody = mprintf("Insert new content here...");
491
}
492
if( isNew ){
493
style_header("New Tech-note %S", zId);
494
}else{
495
style_header("Edit Tech-note %S", zId);
496
}
497
if( P("preview")!=0 ){
498
Blob com;
499
@ <p><b>Timeline comment preview:</b></p>
500
@ <blockquote>
501
@ <table border="0">
502
if( zClrFlag[0] && zClr && zClr[0] ){
503
@ <tr><td style="background-color: %h(zClr);">
504
}else{
505
@ <tr><td>
506
}
507
blob_zero(&com);
508
blob_append(&com, zComment, -1);
509
wiki_convert(&com, 0, WIKI_INLINE|WIKI_NOBADLINKS);
510
@ </td></tr></table>
511
@ </blockquote>
512
@ <p><b>Page content preview:</b><p>
513
@ <blockquote>
514
blob_init(&event, 0, 0);
515
blob_append(&event, zBody, -1);
516
safe_html_context(DOCSRC_WIKI);
517
wiki_render_by_mimetype(&event, zMimetype);
518
@ </blockquote><hr>
519
blob_reset(&event);
520
}
521
for(n=2, z=zBody; z[0]; z++){
522
if( z[0]=='\n' ) n++;
523
}
524
if( n<20 ) n = 20;
525
if( n>40 ) n = 40;
526
@ <form method="post" action="%R/technoteedit"><div>
527
login_insert_csrf_secret();
528
@ <input type="hidden" name="name" value="%h(zId)">
529
@ <table border="0" cellspacing="10">
530
531
@ <tr><th align="right" valign="top">Timestamp (UTC):</th>
532
@ <td valign="top">
533
@ <input type="text" name="t" size="25" value="%h(zETime)">
534
@ </td></tr>
535
536
@ <tr><th align="right" valign="top">Timeline Comment:</th>
537
@ <td valign="top">
538
@ <textarea name="c" class="technoteedit" cols="80"
539
@ rows="3" wrap="virtual">%h(zComment)</textarea>
540
@ </td></tr>
541
542
@ <tr><th align="right" valign="top">Timeline Background Color:</th>
543
@ <td valign="top">
544
@ <input type='checkbox' name='newclr'%s(zClrFlag)>
545
@ Use custom color: \
546
@ <input type='color' name='clr' value='%s(zClr[0]?zClr:"#c0f0ff")'>
547
@ </td></tr>
548
549
@ <tr><th align="right" valign="top">Tags:</th>
550
@ <td valign="top">
551
@ <input type="text" name="g" size="40" value="%h(zTags)">
552
@ </td></tr>
553
554
@ <tr><th align="right" valign="top">\
555
@ %z(href("%R/markup_help"))Markup Style</a>:</th>
556
@ <td valign="top">
557
mimetype_option_menu(zMimetype, "mimetype");
558
@ </td></tr>
559
560
@ <tr><th align="right" valign="top">Page&nbsp;Content:</th>
561
@ <td valign="top">
562
@ <textarea name="w" class="technoteedit" cols="80"
563
@ rows="%d(n)" wrap="virtual">%h(zBody)</textarea>
564
@ </td></tr>
565
566
@ <tr><td colspan="2">
567
@ <input type="submit" name="cancel" value="Cancel">
568
@ <input type="submit" name="preview" value="Preview">
569
if( P("preview") ){
570
@ <input type="submit" name="submit" value="Submit">
571
}
572
@ </td></tr></table>
573
@ </div></form>
574
style_finish_page();
575
}
576
577
/*
578
** Add a new tech note to the repository. The timestamp is
579
** given by the zETime parameter. rid must be zero to create
580
** a new page. If no previous page with the name zPageName exists
581
** and isNew is false, then this routine throws an error.
582
*/
583
void event_cmd_commit(
584
char *zETime, /* timestamp */
585
int rid, /* Artifact id of the tech note */
586
Blob *pContent, /* content of the new page */
587
const char *zMimeType, /* mimetype of the content */
588
const char *zComment, /* comment to go on the timeline */
589
const char *zTags, /* tags */
590
const char *zClr /* background color */
591
){
592
const char *zId; /* id of the tech note */
593
594
if ( rid==0 ){
595
zId = db_text(0, "SELECT lower(hex(randomblob(20)))");
596
}else{
597
zId = db_text(0,
598
"SELECT substr(tagname,7) FROM tag"
599
" WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')",
600
rid
601
);
602
}
603
604
user_select();
605
if (event_commit_common(rid, zId, blob_str(pContent), zETime,
606
zMimeType, zComment, zTags, zClr)==0 ){
607
#ifdef FOSSIL_ENABLE_JSON
608
g.json.resultCode = FSL_JSON_E_ASSERT;
609
#endif
610
fossil_panic("Internal error: Fossil tried to make an "
611
"invalid artifact for the technote.");
612
}
613
}
614

Keyboard Shortcuts

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