Fossil SCM

fossil-scm / src / wiki.c
Blame History Raw 2661 lines
1
/*
2
** Copyright (c) 2007 D. Richard Hipp
3
** Copyright (c) 2008 Stephan Beal
4
**
5
** This program is free software; you can redistribute it and/or
6
** modify it under the terms of the Simplified BSD License (also
7
** known as the "2-Clause License" or "FreeBSD License".)
8
**
9
** This program is distributed in the hope that it will be useful,
10
** but without any warranty; without even the implied warranty of
11
** merchantability or fitness for a particular purpose.
12
**
13
** Author contact information:
14
** [email protected]
15
** http://www.hwaci.com/drh/
16
**
17
*******************************************************************************
18
**
19
** This file contains code to do formatting of wiki text.
20
*/
21
#include "config.h"
22
#include <assert.h>
23
#include <ctype.h>
24
#include "wiki.h"
25
26
#define has_prefix(literal_prfx, zStr) \
27
(fossil_strncmp((zStr), "" literal_prfx, (sizeof literal_prfx)-1)==0)
28
29
/*
30
** Return true if the input string is a well-formed wiki page name.
31
**
32
** Well-formed wiki page names do not begin or end with whitespace,
33
** and do not contain tabs or other control characters and do not
34
** contain more than a single space character in a row. Well-formed
35
** names must be between 1 and 100 characters in length, inclusive.
36
*/
37
int wiki_name_is_wellformed(const unsigned char *z){
38
int i;
39
if( z[0]<=0x20 ){
40
return 0;
41
}
42
for(i=1; z[i]; i++){
43
if( z[i]<0x20 ) return 0;
44
if( z[i]==0x20 && z[i-1]==0x20 ) return 0;
45
}
46
if( z[i-1]==' ' ) return 0;
47
if( i<1 || i>100 ) return 0;
48
return 1;
49
}
50
51
/*
52
** Output rules for well-formed wiki pages
53
*/
54
static void well_formed_wiki_name_rules(void){
55
@ <ul>
56
@ <li> Must not begin or end with a space.</li>
57
@ <li> Must not contain any control characters, including tab or
58
@ newline.</li>
59
@ <li> Must not have two or more spaces in a row internally.</li>
60
@ <li> Must be between 1 and 100 characters in length.</li>
61
@ </ul>
62
}
63
64
/*
65
** Check a wiki name. If it is not well-formed, then issue an error
66
** and return true. If it is well-formed, return false.
67
*/
68
static int check_name(const char *z){
69
if( !wiki_name_is_wellformed((const unsigned char *)z) ){
70
style_set_current_feature("wiki");
71
style_header("Wiki Page Name Error");
72
@ The wiki name "<span class="wikiError">%h(z)</span>" is not well-formed.
73
@ Rules for wiki page names:
74
well_formed_wiki_name_rules();
75
style_finish_page();
76
return 1;
77
}
78
return 0;
79
}
80
81
/*
82
** Return the tagid associated with a particular wiki page.
83
*/
84
int wiki_tagid(const char *zPageName){
85
return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q'",zPageName);
86
}
87
int wiki_tagid2(const char *zPrefix, const char *zPageName){
88
return db_int(0, "SELECT tagid FROM tag WHERE tagname='wiki-%q/%q'",
89
zPrefix, zPageName);
90
}
91
92
/*
93
** Return the RID of the next or previous version of a wiki page.
94
** Return 0 if rid is the last/first version.
95
*/
96
int wiki_next(int tagid, double mtime){
97
return db_int(0,
98
"SELECT srcid FROM tagxref"
99
" WHERE tagid=%d AND mtime>%.16g"
100
" ORDER BY mtime ASC LIMIT 1",
101
tagid, mtime);
102
}
103
int wiki_prev(int tagid, double mtime){
104
return db_int(0,
105
"SELECT srcid FROM tagxref"
106
" WHERE tagid=%d AND mtime<%.16g"
107
" ORDER BY mtime DESC LIMIT 1",
108
tagid, mtime);
109
}
110
111
/*
112
** WEBPAGE: home
113
** WEBPAGE: index
114
** WEBPAGE: not_found
115
**
116
** The /home, /index, and /not_found pages all redirect to the homepage
117
** configured by the administrator.
118
*/
119
void home_page(void){
120
char *zPageName = db_get("project-name",0);
121
char *zIndexPage = db_get("index-page",0);
122
login_check_credentials();
123
cgi_check_for_malice();
124
if( zIndexPage ){
125
const char *zPathInfo = P("PATH_INFO");
126
while( zIndexPage[0]=='/' ) zIndexPage++;
127
while( zPathInfo[0]=='/' ) zPathInfo++;
128
if( fossil_strcmp(zIndexPage, zPathInfo)==0 ) zIndexPage = 0;
129
}
130
if( zIndexPage ){
131
cgi_redirectf("%R/%s", zIndexPage);
132
}
133
if( !g.perm.RdWiki ){
134
cgi_redirectf("%R/login?g=home");
135
}
136
if( zPageName ){
137
login_check_credentials();
138
g.zExtra = zPageName;
139
cgi_set_parameter_nocopy("name", g.zExtra, 1);
140
g.isHome = 1;
141
wiki_page();
142
return;
143
}
144
style_set_current_feature("wiki");
145
style_header("Home");
146
@ <p>This is a stub home-page for the project.
147
@ To fill in this page, first go to
148
@ %z(href("%R/setup_config"))setup/config</a>
149
@ and establish a "Project Name". Then create a
150
@ wiki page with that name. The content of that wiki page
151
@ will be displayed in place of this message.</p>
152
style_finish_page();
153
}
154
155
/*
156
** Return true if the given pagename is the name of the sandbox
157
*/
158
static int is_sandbox(const char *zPagename){
159
return fossil_stricmp(zPagename,"sandbox")==0 ||
160
fossil_stricmp(zPagename,"sand box")==0;
161
}
162
163
/*
164
** Formal, common and short names for the various wiki styles.
165
*/
166
static const char *const azStyles[] = {
167
"text/x-fossil-wiki", "Fossil Wiki", "wiki",
168
"text/x-markdown", "Markdown", "markdown",
169
"text/plain", "Plain Text", "plain"
170
};
171
172
/*
173
** Only allow certain mimetypes through.
174
** All others become "text/x-fossil-wiki"
175
*/
176
const char *wiki_filter_mimetypes(const char *zMimetype){
177
if( zMimetype!=0 ){
178
int i;
179
for(i=0; i<count(azStyles); i+=3){
180
if( fossil_strcmp(zMimetype,azStyles[i+2])==0 ){
181
return azStyles[i];
182
}
183
}
184
if( fossil_strcmp(zMimetype, "text/x-markdown")==0
185
|| fossil_strcmp(zMimetype, "text/plain")==0 ){
186
return zMimetype;
187
}
188
}
189
return "text/x-fossil-wiki";
190
}
191
192
/*
193
** Render wiki text according to its mimetype.
194
**
195
** text/x-fossil-wiki Fossil wiki
196
** text/x-markdown Markdown
197
** text/x-pikchr Pikchr
198
** anything else... Plain text
199
**
200
** If zMimetype is a null pointer, then use "text/x-fossil-wiki".
201
*/
202
void wiki_render_by_mimetype(Blob *pWiki, const char *zMimetype){
203
if( zMimetype==0 || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
204
wiki_convert(pWiki, 0, 0);
205
}else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
206
Blob tail = BLOB_INITIALIZER;
207
markdown_to_html(pWiki, 0, &tail);
208
safe_html(&tail);
209
@ %s(blob_str(&tail))
210
blob_reset(&tail);
211
}else if( fossil_strcmp(zMimetype, "text/x-pikchr")==0 ){
212
int isPopup = P("popup")!=0;
213
const char *zPikchr = blob_str(pWiki);
214
int w, h;
215
char *zOut = pikchr(zPikchr, "pikchr", 0, &w, &h);
216
if( w>0 ){
217
if( isPopup ) cgi_set_content_type("image/svg+xml");
218
else{
219
@ <div class="pikchr-svg" style="max-width:%d(w)px">
220
}
221
@ %s(zOut)
222
if( !isPopup){
223
@ </div>
224
}
225
}else{
226
@ <pre class='error'>
227
@ %h(zOut)
228
@ </pre>
229
}
230
free(zOut);
231
}else{
232
@ <pre class='textPlain'>
233
@ %h(blob_str(pWiki))
234
@ </pre>
235
}
236
}
237
238
/*
239
** WEBPAGE: md_rules
240
**
241
** Show a summary of the Markdown wiki formatting rules.
242
*/
243
void markdown_rules_page(void){
244
Blob x;
245
int fTxt = P("txt")!=0;
246
style_set_current_feature("wiki");
247
style_header("Markdown Formatting Rules");
248
if( fTxt ){
249
style_submenu_element("Formatted", "%R/md_rules");
250
}else{
251
style_submenu_element("Plain-Text", "%R/md_rules?txt=1");
252
}
253
style_submenu_element("Wiki", "%R/wiki_rules");
254
blob_init(&x, builtin_text("markdown.md"), -1);
255
blob_materialize(&x);
256
interwiki_append_map_table(&x);
257
safe_html_context(DOCSRC_TRUSTED);
258
wiki_render_by_mimetype(&x, fTxt ? "text/plain" : "text/x-markdown");
259
blob_reset(&x);
260
style_finish_page();
261
}
262
263
/*
264
** WEBPAGE: wiki_rules
265
**
266
** Show a summary of the wiki formatting rules.
267
*/
268
void wiki_rules_page(void){
269
Blob x;
270
int fTxt = P("txt")!=0;
271
style_set_current_feature("wiki");
272
style_header("Wiki Formatting Rules");
273
if( fTxt ){
274
style_submenu_element("Formatted", "%R/wiki_rules");
275
}else{
276
style_submenu_element("Plain-Text", "%R/wiki_rules?txt=1");
277
}
278
style_submenu_element("Markdown","%R/md_rules");
279
blob_init(&x, builtin_text("wiki.wiki"), -1);
280
blob_materialize(&x);
281
interwiki_append_map_table(&x);
282
safe_html_context(DOCSRC_TRUSTED);
283
wiki_render_by_mimetype(&x, fTxt ? "text/plain" : "text/x-fossil-wiki");
284
blob_reset(&x);
285
style_finish_page();
286
}
287
288
/*
289
** WEBPAGE: markup_help
290
**
291
** Show links to the md_rules and wiki_rules pages.
292
*/
293
void markup_help_page(void){
294
style_set_current_feature("wiki");
295
style_header("Fossil Markup Styles");
296
@ <ul>
297
@ <li><p>%z(href("%R/wiki_rules"))Fossil Wiki Formatting Rules</a></p></li>
298
@ <li><p>%z(href("%R/md_rules"))Markdown Formatting Rules</a></p></li>
299
@ </ul>
300
style_finish_page();
301
}
302
303
/*
304
** Returns non-zero if moderation is required for wiki changes and wiki
305
** attachments.
306
*/
307
int wiki_need_moderation(
308
int localUser /* Are we being called for a local interactive user? */
309
){
310
/*
311
** If the FOSSIL_FORCE_WIKI_MODERATION variable is set, *ALL* changes for
312
** wiki pages will be required to go through moderation (even those performed
313
** by the local interactive user via the command line). This can be useful
314
** for local (or remote) testing of the moderation subsystem and its impact
315
** on the contents and status of wiki pages.
316
*/
317
if( fossil_getenv("FOSSIL_FORCE_WIKI_MODERATION")!=0 ){
318
return 1;
319
}
320
if( localUser ){
321
return 0;
322
}
323
return g.perm.ModWiki==0 && db_get_boolean("modreq-wiki",0)==1;
324
}
325
326
/* Standard submenu items for wiki pages */
327
#define W_SRCH 0x00001
328
#define W_LIST 0x00002
329
#define W_HELP 0x00004
330
#define W_NEW 0x00008
331
#define W_BLOG 0x00010
332
#define W_SANDBOX 0x00020
333
#define W_ALL 0x0001f
334
#define W_ALL_BUT(x) (W_ALL&~(x))
335
336
/*
337
** Add some standard submenu elements for wiki screens.
338
*/
339
static void wiki_standard_submenu(unsigned int ok){
340
if( (ok & W_SRCH)!=0 && search_restrict(SRCH_WIKI)!=0 ){
341
style_submenu_element("Search", "%R/wikisrch");
342
}
343
if( (ok & W_LIST)!=0 ){
344
style_submenu_element("List", "%R/wcontent");
345
}
346
if( (ok & W_HELP)!=0 ){
347
style_submenu_element("Help", "%R/wikihelp");
348
}
349
if( (ok & W_NEW)!=0 && g.anon.NewWiki ){
350
style_submenu_element("New", "%R/wikinew");
351
}
352
if( (ok & W_SANDBOX)!=0 ){
353
style_submenu_element("Sandbox", "%R/wikiedit?name=Sandbox");
354
}
355
}
356
357
/*
358
** WEBPAGE: wikihelp
359
** A generic landing page for wiki.
360
*/
361
void wiki_helppage(void){
362
login_check_credentials();
363
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
364
style_set_current_feature("wiki");
365
style_header("Wiki Help");
366
wiki_standard_submenu(W_ALL_BUT(W_HELP));
367
@ <h2>Wiki Links</h2>
368
@ <ul>
369
@ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li>
370
@ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for
371
@ %z(href("%R/md_rules"))Markdown Wiki</a>.</li>
372
@ <li> Use the %z(href("%R/wikiedit?name=Sandbox"))Sandbox</a>
373
@ to experiment.</li>
374
if( g.perm.NewWiki ){
375
@ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li>
376
if( g.perm.Write ){
377
@ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li>
378
}
379
}
380
@ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a>
381
@ available on this server.</li>
382
@ <li> %z(href("%R/timeline?y=e"))List of All Tech-notes</a>
383
@ available on this server.</li>
384
if( g.perm.ModWiki ){
385
@ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li>
386
}
387
if( search_restrict(SRCH_WIKI)!=0 ){
388
@ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key
389
@ words</li>
390
}
391
@ </ul>
392
style_finish_page();
393
return;
394
}
395
396
/*
397
** WEBPAGE: wikisrch
398
** Usage: /wikisrch?s=PATTERN
399
**
400
** Full-text search of all current wiki text
401
*/
402
void wiki_srchpage(void){
403
login_check_credentials();
404
style_set_current_feature("wiki");
405
style_header("Wiki Search");
406
wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX);
407
search_screen(SRCH_WIKI, 0);
408
style_finish_page();
409
}
410
411
/* Return values from wiki_page_type() */
412
#if INTERFACE
413
# define WIKITYPE_UNKNOWN (-1)
414
# define WIKITYPE_NORMAL 0
415
# define WIKITYPE_BRANCH 1
416
# define WIKITYPE_CHECKIN 2
417
# define WIKITYPE_TAG 3
418
# define WIKITYPE_TICKET 4
419
#endif
420
421
/*
422
** Figure out what type of wiki page we are dealing with.
423
*/
424
int wiki_page_type(const char *zPageName){
425
if( db_get_boolean("wiki-about",1)==0 ){
426
return WIKITYPE_NORMAL;
427
}else
428
if( has_prefix("checkin/", zPageName)
429
&& db_exists("SELECT 1 FROM blob WHERE uuid=%Q",zPageName+8)
430
){
431
return WIKITYPE_CHECKIN;
432
}else
433
if( has_prefix("branch/", zPageName) ){
434
return WIKITYPE_BRANCH;
435
}else
436
if( has_prefix("tag/", zPageName) ){
437
return WIKITYPE_TAG;
438
}else
439
if( has_prefix("ticket/", zPageName) ){
440
return WIKITYPE_TICKET;
441
}
442
return WIKITYPE_NORMAL;
443
}
444
445
/*
446
** Returns a JSON-friendly string form of the integer value returned
447
** by wiki_page_type(zPageName).
448
*/
449
const char * wiki_page_type_name(const char *zPageName){
450
switch(wiki_page_type(zPageName)){
451
case WIKITYPE_CHECKIN: return "checkin";
452
case WIKITYPE_BRANCH: return "branch";
453
case WIKITYPE_TAG: return "tag";
454
case WIKITYPE_TICKET: return "ticket";
455
case WIKITYPE_NORMAL:
456
default: return "normal";
457
}
458
}
459
460
/*
461
** Add an appropriate style_header() for either the /wiki or /wikiedit page
462
** for zPageName. zExtra is an empty string for /wiki but has the text
463
** "Edit: " for /wikiedit.
464
**
465
** If the page is /wiki and the page is one of the special times (check-in,
466
** branch, or tag) and the "p" query parameter is omitted, then do a
467
** redirect to the display of the check-in, branch, or tag rather than
468
** continuing to the plain wiki display.
469
*/
470
static int wiki_page_header(
471
int eType, /* Page type. Might be WIKITYPE_UNKNOWN */
472
const char *zPageName, /* Name of the page */
473
const char *zExtra /* Extra prefix text on the page header */
474
){
475
style_set_current_feature("wiki");
476
if( eType==WIKITYPE_UNKNOWN ) eType = wiki_page_type(zPageName);
477
switch( eType ){
478
case WIKITYPE_NORMAL: {
479
style_header("%s%s", zExtra, zPageName);
480
break;
481
}
482
case WIKITYPE_CHECKIN: {
483
zPageName += 8;
484
if( zExtra[0]==0 && !P("p") ){
485
cgi_redirectf("%R/info/%s",zPageName);
486
}else{
487
style_header("Notes About Check-in %S", zPageName);
488
style_submenu_element("Check-in Timeline","%R/timeline?f=%s",
489
zPageName);
490
style_submenu_element("Check-in Info","%R/info/%s", zPageName);
491
}
492
break;
493
}
494
case WIKITYPE_BRANCH: {
495
zPageName += 7;
496
if( zExtra[0]==0 && !P("p") ){
497
cgi_redirectf("%R/timeline?r=%t", zPageName);
498
}else{
499
style_header("Notes About Branch %h", zPageName);
500
style_submenu_element("Branch Timeline","%R/timeline?r=%t", zPageName);
501
}
502
break;
503
}
504
case WIKITYPE_TAG: {
505
zPageName += 4;
506
if( zExtra[0]==0 && !P("p") ){
507
cgi_redirectf("%R/timeline?t=%t",zPageName);
508
}else{
509
style_header("Notes About Tag %h", zPageName);
510
style_submenu_element("Tag Timeline","%R/timeline?t=%t",zPageName);
511
}
512
break;
513
}
514
case WIKITYPE_TICKET: {
515
zPageName += 7;
516
if( zExtra[0]==0 && !P("p") ){
517
cgi_redirectf("%R/tktview/%s",zPageName);
518
}else{
519
style_header("Notes About Ticket %h", zPageName);
520
style_submenu_element("Ticket","%R/tktview/%s",zPageName);
521
}
522
break;
523
}
524
}
525
return eType;
526
}
527
528
/*
529
** Wiki pages with special names "branch/...", "checkin/...", and "tag/..."
530
** requires perm.Write privilege in addition to perm.WrWiki in order
531
** to write. This function determines whether the extra perm.Write
532
** is required and available. Return true if writing to the wiki page
533
** may proceed, and return false if permission is lacking.
534
*/
535
static int wiki_special_permission(const char *zPageName){
536
if( strncmp(zPageName,"branch/",7)!=0
537
&& strncmp(zPageName,"checkin/",8)!=0
538
&& strncmp(zPageName,"tag/",4)!=0
539
&& strncmp(zPageName,"ticket/",7)!=0
540
){
541
return 1;
542
}
543
if( db_get_boolean("wiki-about",1)==0 ){
544
return 1;
545
}
546
if( strncmp(zPageName,"ticket/",7)==0 ){
547
return g.perm.WrTkt;
548
}
549
return g.perm.Write;
550
}
551
552
/*
553
** WEBPAGE: wiki
554
**
555
** Display a wiki page. Example: /wiki?name=PAGENAME
556
**
557
** Query parameters:
558
**
559
** name=NAME Name of the wiki page to display. Required.
560
** nsm Omit the submenu if present. (Mnemonic: No SubMenu)
561
** p Always show just the wiki page. For special
562
** pages for check-ins, branches, or tags, there will
563
** be a redirect to the associated /info page unless
564
** this query parameter is present.
565
** popup Suppress the header and footer and other page
566
** boilerplate and only return the formatted content
567
** of the wiki page.
568
*/
569
void wiki_page(void){
570
char *zTag;
571
int rid = 0;
572
int isSandbox;
573
unsigned submenuFlags = W_HELP;
574
Blob wiki;
575
Manifest *pWiki = 0;
576
const char *zPageName;
577
const char *zMimetype = 0;
578
int isPopup = P("popup")!=0;
579
char *zBody = fossil_strdup("<i>Empty Page</i>");
580
int noSubmenu = P("nsm")!=0 || g.isHome;
581
582
login_check_credentials();
583
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
584
zPageName = P("name");
585
(void)P("s")/*for cgi_check_for_malice(). "s" == search stringy*/;
586
cgi_check_for_malice();
587
if( zPageName==0 ){
588
if( search_restrict(SRCH_WIKI)!=0 ){
589
wiki_srchpage();
590
}else{
591
wiki_helppage();
592
}
593
return;
594
}
595
if( check_name(zPageName) ) return;
596
isSandbox = is_sandbox(zPageName);
597
if( isSandbox ){
598
submenuFlags &= ~W_SANDBOX;
599
zBody = db_get("sandbox",zBody);
600
zMimetype = db_get("sandbox-mimetype","text/x-fossil-wiki");
601
rid = 0;
602
}else{
603
const char *zUuid = P("id");
604
if( zUuid==0 || (rid = symbolic_name_to_rid(zUuid,"w"))==0 ){
605
zTag = mprintf("wiki-%s", zPageName);
606
rid = db_int(0,
607
"SELECT rid FROM tagxref"
608
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
609
" ORDER BY mtime DESC", zTag
610
);
611
free(zTag);
612
}
613
pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
614
if( pWiki ){
615
zBody = pWiki->zWiki;
616
zMimetype = pWiki->zMimetype;
617
}
618
}
619
zMimetype = wiki_filter_mimetypes(zMimetype);
620
if( !noSubmenu ){
621
if( ((rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki))
622
&& wiki_special_permission(zPageName)
623
){
624
style_submenu_element("Edit", "%R/wikiedit?name=%T", zPageName);
625
}else if( rid && g.perm.ApndWiki ){
626
style_submenu_element("Edit", "%R/wikiappend?name=%T", zPageName);
627
}
628
if( g.perm.Hyperlink ){
629
style_submenu_element("History", "%R/whistory?name=%T", zPageName);
630
}
631
if( rid>0 && attach_user_may(rid, CFTYPE_WIKI) ){
632
style_submenu_element("Attach", "%R/attachadd?target=%T",
633
zPageName);
634
}
635
}
636
if( !isPopup ){
637
style_set_current_page("%T?name=%T", g.zPath, zPageName);
638
wiki_page_header(WIKITYPE_UNKNOWN, zPageName, "");
639
if( !noSubmenu ){
640
wiki_standard_submenu(submenuFlags);
641
}
642
}
643
if( zBody[0]==0 ){
644
@ <i>This page has been deleted</i>
645
}else{
646
blob_init(&wiki, zBody, -1);
647
safe_html_context(DOCSRC_WIKI);
648
wiki_render_by_mimetype(&wiki, zMimetype);
649
blob_reset(&wiki);
650
}
651
manifest_destroy(pWiki);
652
if( !isPopup ){
653
char * zLabel = mprintf("<h2><a href='%R/attachlist?page=%T'>"
654
"Attachments</a>:</h2>",
655
zPageName);
656
attachment_list(zPageName, zLabel, 1);
657
fossil_free(zLabel);
658
document_emit_js(/*for optional pikchr support*/);
659
style_finish_page();
660
}
661
}
662
663
/*
664
** Write a wiki artifact into the repository
665
*/
666
int wiki_put(Blob *pWiki, int parent, int needMod){
667
int nrid;
668
if( !needMod ){
669
nrid = content_put_ex(pWiki, 0, 0, 0, 0);
670
if( parent ) content_deltify(parent, &nrid, 1, 0);
671
}else{
672
nrid = content_put_ex(pWiki, 0, 0, 0, 1);
673
moderation_table_create();
674
db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid);
675
}
676
db_add_unsent(nrid);
677
db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid);
678
manifest_crosslink(nrid, pWiki, MC_NONE);
679
if( login_is_individual() ){
680
alert_user_contact(login_name());
681
}
682
return nrid;
683
}
684
685
/*
686
** Output a selection box from which the user can select the
687
** wiki mimetype. Arguments:
688
**
689
** zMimetype - The current value of the query parameter
690
** zParam - The name of the query parameter
691
*/
692
void mimetype_option_menu(const char *zMimetype, const char *zParam){
693
unsigned i;
694
@ <select name="%s(zParam)" size="1">
695
for(i=0; i<count(azStyles); i+=3){
696
if( fossil_strcmp(zMimetype,azStyles[i])==0 ){
697
@ <option value="%s(azStyles[i])" selected>%s(azStyles[i+1])</option>
698
}else{
699
@ <option value="%s(azStyles[i])">%s(azStyles[i+1])</option>
700
}
701
}
702
@ </select>
703
}
704
705
/*
706
** Given a mimetype, return its common name.
707
*/
708
static const char *mimetype_common_name(const char *zMimetype){
709
int i;
710
for(i=6; i>=0; i-=3){
711
if( zMimetype && fossil_strcmp(zMimetype, azStyles[i])==0 ){
712
return azStyles[i+1];
713
}
714
}
715
return azStyles[1];
716
}
717
718
/*
719
** Tries to fetch a wiki page for the given name. If found, it
720
** returns true, else false.
721
**
722
** versionsBack specifies how many versions back in the history to
723
** fetch. Use 0 for the latest version, 1 for its parent, etc.
724
**
725
** If pRid is not NULL then if a result is found *pRid is set to its
726
** RID. If ppWiki is not NULL then if found *ppWiki is set to the
727
** loaded wiki object, which the caller is responsible for passing to
728
** manifest_destroy().
729
*/
730
static int wiki_fetch_by_name( const char *zPageName,
731
unsigned int versionsBack,
732
int * pRid, Manifest **ppWiki ){
733
Manifest *pWiki = 0;
734
char *zTag = mprintf("wiki-%s", zPageName);
735
Stmt q = empty_Stmt;
736
int rid = 0;
737
738
db_prepare(&q, "SELECT rid FROM tagxref"
739
" WHERE tagid=(SELECT tagid FROM tag WHERE"
740
" tagname=%Q) "
741
" ORDER BY mtime DESC LIMIT -1 OFFSET %u", zTag,
742
versionsBack);
743
fossil_free(zTag);
744
if(SQLITE_ROW == db_step(&q)){
745
rid = db_column_int(&q, 0);
746
}
747
db_finalize(&q);
748
if( rid == 0 ){
749
return 0;
750
}
751
else if(pRid){
752
*pRid = rid;
753
}
754
if(ppWiki){
755
pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
756
if( pWiki==0 ){
757
/* "Cannot happen." */
758
return 0;
759
}
760
*ppWiki = pWiki;
761
}
762
return 1;
763
}
764
765
/*
766
** Determines whether the wiki page with the given name can be edited
767
** or created by the current user. If not, an AJAX error is queued and
768
** false is returned, else true is returned. A NULL, empty, or
769
** malformed name is considered non-writable, regardless of the user.
770
**
771
** If pRid is not NULL then this function writes the page's rid to
772
** *pRid (whether or not access is granted). On error or if the page
773
** does not yet exist, *pRid will be set to 0.
774
**
775
** Note that the sandbox is a special case: it is a pseudo-page with
776
** no rid and the /wikiajax API does not allow anyone to actually save
777
** a sandbox page, but it is reported as writable here (with rid 0).
778
*/
779
static int wiki_ajax_can_write(const char *zPageName, int * pRid){
780
int rid = 0;
781
const char * zErr = 0;
782
783
if(pRid) *pRid = 0;
784
if(!zPageName || !*zPageName
785
|| !wiki_name_is_wellformed((unsigned const char *)zPageName)){
786
zErr = "Invalid page name.";
787
}else if(is_sandbox(zPageName)){
788
return 1;
789
}else{
790
wiki_fetch_by_name(zPageName, 0, &rid, 0);
791
if(pRid) *pRid = rid;
792
if(!wiki_special_permission(zPageName)){
793
zErr = "Editing this page requires non-wiki write permissions.";
794
}else if( (rid && g.perm.WrWiki) || (!rid && g.perm.NewWiki) ){
795
return 3;
796
}else if(rid && !g.perm.WrWiki){
797
zErr = "Requires wiki-write permissions.";
798
}else if(!rid && !g.perm.NewWiki){
799
zErr = "Requires new-wiki permissions.";
800
}else{
801
zErr = "Cannot happen! Please report this as a bug.";
802
}
803
}
804
ajax_route_error(403, "%s", zErr);
805
return 0;
806
}
807
808
809
/*
810
** Emits an array of attachment info records for the given wiki page
811
** artifact.
812
**
813
** Output format:
814
**
815
** [{
816
** "uuid": attachment artifact hash,
817
** "src": hash of the attachment blob,
818
** "target": wiki page name or ticket/event ID,
819
** "filename": filename of attachment,
820
** "mtime": ISO-8601 timestamp UTC,
821
** "isLatest": true this is the latest version of this file
822
** else false,
823
** }, ...once per attachment]
824
**
825
** If there are no matching attachments then it will emit a JSON
826
** null (if nullIfEmpty) or an empty JSON array.
827
**
828
** If latestOnly is true then only the most recent entry for a given
829
** attachment is emitted, else all versions are emitted in descending
830
** mtime order.
831
*/
832
static void wiki_ajax_emit_page_attachments(Manifest * pWiki,
833
int latestOnly,
834
int nullIfEmpty){
835
Blob b = BLOB_INITIALIZER;
836
attachments_to_json(pWiki, &b, latestOnly,
837
nullIfEmpty ? -1 : 1);
838
CX("%b", &b);
839
blob_reset(&b);
840
}
841
842
/*
843
** Proxy for wiki_ajax_emit_page_attachments() which attempts to load
844
** the given wiki page artifact. Returns true if it can load the given
845
** page, else false. If it returns false then it queues up a 404 ajax
846
** error response.
847
*/
848
static int wiki_ajax_emit_page_attachments2(const char *zPageName,
849
int latestOnly,
850
int nullIfEmpty){
851
Manifest * pWiki = 0;
852
if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
853
ajax_route_error(404, "Wiki page could not be loaded: %s",
854
zPageName);
855
return 0;
856
}
857
wiki_ajax_emit_page_attachments(pWiki, latestOnly, nullIfEmpty);
858
manifest_destroy(pWiki);
859
return 1;
860
}
861
862
863
/*
864
** Loads the given wiki page, sets the response type to
865
** application/json, and emits it as a JSON object. If zPageName is a
866
** sandbox page then a "fake" object is emitted, as the wikiajax API
867
** does not permit saving the sandbox.
868
**
869
** Returns true on success, false on error, and on error it
870
** queues up a JSON-format error response.
871
**
872
** Output JSON format:
873
**
874
** { name: "page name",
875
** type: "normal" | "tag" | "checkin" | "branch" | "sandbox",
876
** mimetype: "mimetype",
877
** version: UUID string or null for a sandbox page,
878
** parent: "parent uuid" or null if no parent,
879
** isDeleted: true if the page has no content (is "deleted")
880
** else not set (making it "falsy" in JS),
881
** attachments: see wiki_ajax_emit_page_attachments(),
882
** content: "page content" (only if includeContent is true)
883
** }
884
**
885
** If includeContent is false then the content member is elided.
886
*/
887
static int wiki_ajax_emit_page_object(const char *zPageName,
888
int includeContent){
889
Manifest * pWiki = 0;
890
char * zUuid;
891
892
if( is_sandbox(zPageName) ){
893
char * zMimetype =
894
db_get("sandbox-mimetype","text/x-fossil-wiki");
895
char * zBody = db_get("sandbox","");
896
CX("{\"name\": %!j, \"type\": \"sandbox\", "
897
"\"mimetype\": %!j, \"version\": null, \"parent\": null",
898
zPageName, zMimetype);
899
if(includeContent){
900
CX(", \"content\": %!j",
901
zBody);
902
}
903
CX("}");
904
fossil_free(zMimetype);
905
fossil_free(zBody);
906
return 1;
907
}else if( !wiki_fetch_by_name(zPageName, 0, 0, &pWiki) ){
908
ajax_route_error(404, "Wiki page could not be loaded: %s",
909
zPageName);
910
return 0;
911
}else{
912
zUuid = rid_to_uuid(pWiki->rid);
913
CX("{\"name\": %!j, \"type\": %!j, "
914
"\"version\": %!j, "
915
"\"mimetype\": %!j, ",
916
pWiki->zWikiTitle,
917
wiki_page_type_name(pWiki->zWikiTitle),
918
zUuid,
919
pWiki->zMimetype ? pWiki->zMimetype : "text/x-markdown");
920
CX("\"parent\": ");
921
if(pWiki->nParent){
922
CX("%!j", pWiki->azParent[0]);
923
}else{
924
CX("null");
925
}
926
if(!pWiki->zWiki || !pWiki->zWiki[0]){
927
CX(", \"isEmpty\": true");
928
}
929
if(includeContent){
930
CX(", \"content\": %!j", pWiki->zWiki);
931
}
932
CX(", \"attachments\": ");
933
wiki_ajax_emit_page_attachments(pWiki, 0, 1);
934
CX("}");
935
fossil_free(zUuid);
936
manifest_destroy(pWiki);
937
return 2;
938
}
939
}
940
941
/*
942
** Ajax route handler for /wikiajax/save.
943
**
944
** URL params:
945
**
946
** page = the wiki page name.
947
** mimetype = content mimetype.
948
** content = page content. Fossil considers an empty page to
949
** be "deleted".
950
** isnew = 1 if the page is to be newly-created, else 0 or
951
** not send.
952
**
953
** Responds with JSON. On error, an object in the form documented by
954
** ajax_route_error(). On success, an object in the form documented
955
** for wiki_ajax_emit_page_object().
956
**
957
** The wikiajax API disallows saving of a sandbox pseudo-page, and
958
** will respond with an error if asked to save one. Should we want to
959
** enable it, it's implemented like this for any saved page for which
960
** is_sandbox(zPageName) is true:
961
**
962
** db_set("sandbox",zBody,0);
963
** db_set("sandbox-mimetype",zMimetype,0);
964
**
965
*/
966
static void wiki_ajax_route_save(void){
967
const char *zPageName = P("page");
968
const char *zMimetype = P("mimetype");
969
const char *zContent = P("content");
970
const int isNew = ajax_p_bool("isnew");
971
Blob content = empty_blob;
972
int parentRid = 0;
973
int rollback = 0;
974
975
if(!wiki_ajax_can_write(zPageName, &parentRid)){
976
return;
977
}else if(is_sandbox(zPageName)){
978
ajax_route_error(403,"Saving a sandbox page is prohibited.");
979
return;
980
}
981
/* These isNew checks are just me being pedantic. We could just as
982
easily derive isNew based on whether or not the page already
983
exists. */
984
if(isNew){
985
if(parentRid>0){
986
ajax_route_error(403,"Requested a new page, "
987
"but it already exists with RID %d: %s",
988
parentRid, zPageName);
989
return;
990
}
991
}else if(parentRid==0){
992
ajax_route_error(403,"Creating new page [%s] requires passing "
993
"isnew=1.", zPageName);
994
return;
995
}
996
blob_init(&content, zContent ? zContent : "", -1);
997
cgi_set_content_type("application/json");
998
db_begin_transaction();
999
wiki_cmd_commit(zPageName, parentRid, &content, zMimetype, 0);
1000
rollback = wiki_ajax_emit_page_object(zPageName, 1) ? 0 : 1;
1001
db_end_transaction(rollback);
1002
}
1003
1004
/*
1005
** Ajax route handler for /wikiajax/fetch.
1006
**
1007
** URL params:
1008
**
1009
** page = the wiki page name
1010
**
1011
** Responds with JSON. On error, an object in the form documented by
1012
** ajax_route_error(). On success, an object in the form documented
1013
** for wiki_ajax_emit_page_object().
1014
*/
1015
static void wiki_ajax_route_fetch(void){
1016
const char * zPageName = P("page");
1017
1018
if( zPageName==0 || zPageName[0]==0 ){
1019
ajax_route_error(400,"Missing page name.");
1020
return;
1021
}
1022
cgi_set_content_type("application/json");
1023
wiki_ajax_emit_page_object(zPageName, 1);
1024
}
1025
1026
/*
1027
** Ajax route handler for /wikiajax/attachments.
1028
**
1029
** URL params:
1030
**
1031
** page = the wiki page name
1032
** latestOnly = if set, only latest version of each attachment
1033
** is emitted.
1034
**
1035
** Responds with JSON: see wiki_ajax_emit_page_attachments()
1036
**
1037
** If there are no attachments it emits an empty array instead of null
1038
** so that the output can be used as a top-level JSON response.
1039
**
1040
** On error, an object in the form documented by
1041
** ajax_route_error(). On success, an object in the form documented
1042
** for wiki_ajax_emit_page_attachments().
1043
*/
1044
static void wiki_ajax_route_attachments(void){
1045
const char * zPageName = P("page");
1046
const int fLatestOnly = P("latestOnly")!=0;
1047
if( zPageName==0 || zPageName[0]==0 ){
1048
ajax_route_error(400,"Missing page name.");
1049
return;
1050
}
1051
cgi_set_content_type("application/json");
1052
wiki_ajax_emit_page_attachments2(zPageName, fLatestOnly, 0);
1053
}
1054
1055
/*
1056
** Ajax route handler for /wikiajax/diff.
1057
**
1058
** URL params:
1059
**
1060
** page = the wiki page name
1061
** content = the new/edited wiki page content
1062
**
1063
** Requires that the user have write access solely to avoid some
1064
** potential abuse cases. It does not actually write anything.
1065
*/
1066
static void wiki_ajax_route_diff(void){
1067
const char * zPageName = P("page");
1068
Blob contentNew = empty_blob, contentOrig = empty_blob;
1069
Manifest * pParent = 0;
1070
const char * zContent = P("content");
1071
u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR;
1072
char * zParentUuid = 0;
1073
1074
if( zPageName==0 || zPageName[0]==0 ){
1075
ajax_route_error(400,"Missing page name.");
1076
return;
1077
}else if(!wiki_ajax_can_write(zPageName, 0)){
1078
return;
1079
}
1080
switch(atoi(PD("sbs","0"))){
1081
case 0: diffFlags |= DIFF_LINENO; break;
1082
default: diffFlags |= DIFF_SIDEBYSIDE;
1083
}
1084
switch(atoi(PD("ws","2"))){
1085
case 1: diffFlags |= DIFF_IGNORE_EOLWS; break;
1086
case 2: diffFlags |= DIFF_IGNORE_ALLWS; break;
1087
default: break;
1088
}
1089
wiki_fetch_by_name( zPageName, 0, 0, &pParent );
1090
if( pParent ){
1091
zParentUuid = rid_to_uuid(pParent->rid);
1092
}
1093
if( pParent && pParent->zWiki && *pParent->zWiki ){
1094
blob_init(&contentOrig, pParent->zWiki, -1);
1095
}else{
1096
blob_init(&contentOrig, "", 0);
1097
}
1098
blob_init(&contentNew, zContent ? zContent : "", -1);
1099
cgi_set_content_type("text/html");
1100
ajax_render_diff(&contentOrig, zParentUuid, &contentNew, diffFlags);
1101
blob_reset(&contentNew);
1102
blob_reset(&contentOrig);
1103
fossil_free(zParentUuid);
1104
manifest_destroy(pParent);
1105
}
1106
1107
/*
1108
** Ajax route handler for /wikiajax/preview.
1109
**
1110
** URL params:
1111
**
1112
** mimetype = the wiki page mimetype (determines rendering style)
1113
** content = the wiki page content
1114
**
1115
** Responds with a partial HTML document.
1116
*/
1117
static void wiki_ajax_route_preview(void){
1118
const char * zContent = P("content");
1119
1120
if( zContent==0 ){
1121
ajax_route_error(400,"Missing content to preview.");
1122
return;
1123
}else{
1124
Blob content = empty_blob;
1125
const char * zMimetype = PD("mimetype","text/x-fossil-wiki");
1126
1127
blob_init(&content, zContent, -1);
1128
cgi_set_content_type("text/html");
1129
wiki_render_by_mimetype(&content, zMimetype);
1130
blob_reset(&content);
1131
}
1132
}
1133
1134
/*
1135
** Outputs the wiki page list in JSON form. If verbose is false then
1136
** it emits an array of strings (page names). If verbose is true it outputs
1137
** an array of objects in this form:
1138
**
1139
** { name: string, version: string or null of sandbox box,
1140
** parent: uuid or null for first version or sandbox,
1141
** mimetype: string,
1142
** type: string (normal, branch, tag, check-in, or sandbox)
1143
** }
1144
**
1145
** If includeContent is true, the object contains a "content" member
1146
** with the raw page content. includeContent is ignored if verbose is
1147
** false.
1148
**
1149
*/
1150
static void wiki_render_page_list_json(int verbose, int includeContent){
1151
Stmt q = empty_Stmt;
1152
int n = 0;
1153
db_begin_transaction();
1154
db_prepare(&q, "SELECT"
1155
" substr(tagname,6) AS name"
1156
" FROM tag JOIN tagxref USING('tagid')"
1157
" WHERE tagname GLOB 'wiki-*'"
1158
" AND TYPEOF(tagxref.value+0)='integer'"
1159
/* ^^^ elide wiki- tags which are not wiki pages */
1160
" UNION SELECT 'Sandbox' AS name"
1161
" ORDER BY name COLLATE NOCASE");
1162
CX("[");
1163
while( SQLITE_ROW==db_step(&q) ){
1164
char const * zName = db_column_text(&q,0);
1165
if(n++){
1166
CX(",");
1167
}
1168
if(verbose==0){
1169
CX("%!j", zName);
1170
}else{
1171
wiki_ajax_emit_page_object(zName, includeContent);
1172
}
1173
}
1174
CX("]");
1175
db_finalize(&q);
1176
db_end_transaction(0);
1177
}
1178
1179
/*
1180
** Ajax route handler for /wikiajax/list.
1181
**
1182
** Optional parameters: verbose, includeContent (see below).
1183
**
1184
** Responds with JSON. On error, an object in the form documented by
1185
** ajax_route_error().
1186
**
1187
** On success, it emits an array of strings (page names) sorted
1188
** case-insensitively. If the "verbose" parameter is passed in then
1189
** the result list contains objects in the format documented for
1190
** wiki_ajax_emit_page_object(). The content of each object is elided
1191
** unless the "includeContent" parameter is passed on with a
1192
** "non-false" value..
1193
**
1194
** The result list always contains an entry named "Sandbox" which
1195
** represents the sandbox pseudo-page.
1196
*/
1197
static void wiki_ajax_route_list(void){
1198
const int verbose = ajax_p_bool("verbose");
1199
const int includeContent = ajax_p_bool("includeContent");
1200
1201
cgi_set_content_type("application/json");
1202
wiki_render_page_list_json(verbose, includeContent);
1203
}
1204
1205
/*
1206
** WEBPAGE: wikiajax hidden
1207
**
1208
** An internal dispatcher for wiki AJAX operations. Not for direct
1209
** client use. All routes defined by this interface are app-internal,
1210
** subject to change
1211
*/
1212
void wiki_ajax_page(void){
1213
const char * zName = P("name");
1214
AjaxRoute routeName = {0,0,0,0};
1215
const AjaxRoute * pRoute = 0;
1216
const AjaxRoute routes[] = {
1217
/* Keep these sorted by zName (for bsearch()) */
1218
{"attachments", wiki_ajax_route_attachments, 0, 0},
1219
{"diff", wiki_ajax_route_diff, 1, 1},
1220
{"fetch", wiki_ajax_route_fetch, 0, 0},
1221
{"list", wiki_ajax_route_list, 0, 0},
1222
{"preview", wiki_ajax_route_preview, 0, 1},
1223
{"save", wiki_ajax_route_save, 1, 1}
1224
};
1225
1226
if(zName==0 || zName[0]==0){
1227
ajax_route_error(400,"Missing required [route] 'name' parameter.");
1228
return;
1229
}
1230
routeName.zName = zName;
1231
pRoute = (const AjaxRoute *)bsearch(&routeName, routes,
1232
count(routes), sizeof routes[0],
1233
cmp_ajax_route_name);
1234
if(pRoute==0){
1235
ajax_route_error(404,"Ajax route not found.");
1236
return;
1237
}
1238
login_check_credentials();
1239
if( pRoute->bWriteMode!=0 && g.perm.WrWiki==0 ){
1240
ajax_route_error(403,"Write permissions required.");
1241
return;
1242
}else if( pRoute->bWriteMode==0 && g.perm.RdWiki==0 ){
1243
ajax_route_error(403,"Read-Wiki permissions required.");
1244
return;
1245
}else if(0==cgi_csrf_safe(pRoute->bPost)){
1246
ajax_route_error(403,
1247
"CSRF violation (make sure sending of HTTP "
1248
"Referer headers is enabled for XHR "
1249
"connections).");
1250
return;
1251
}
1252
pRoute->xCallback();
1253
}
1254
1255
/*
1256
** Emits a preview-toggle option widget for /wikiedit and /fileedit.
1257
*/
1258
void wikiedit_emit_toggle_preview(void){
1259
CX("<div class='input-with-label'>"
1260
"<input type='checkbox' id='edit-shift-enter-preview' "
1261
"></input><label for='edit-shift-enter-preview'>"
1262
"Shift-enter previews</label>"
1263
"<div class='help-buttonlet'>"
1264
"When enabled, shift-enter switches between preview and edit modes. "
1265
"Some software-based keyboards misinteract with this, so it can be "
1266
"disabled when needed."
1267
"</div>"
1268
"</div>");
1269
}
1270
1271
/*
1272
** WEBPAGE: wikiedit
1273
** URL: /wikedit?name=PAGENAME
1274
**
1275
** The main front-end for the Ajax-based wiki editor app. Passing
1276
** in the name of an unknown page will trigger the creation
1277
** of a new page (which is not actually created in the database
1278
** until the user explicitly saves it). If passed no page name,
1279
** the user may select a page from the list on the first UI tab.
1280
**
1281
** When creating a new page, the mimetype URL parameter may optionally
1282
** be used to set its mimetype to one of text/x-fossil-wiki,
1283
** text/x-markdown, or text/plain, defaulting to the former.
1284
*/
1285
void wikiedit_page(void){
1286
const char *zPageName;
1287
const char * zMimetype = P("mimetype");
1288
int isSandbox;
1289
int found = 0;
1290
1291
login_check_credentials();
1292
zPageName = PD("name","");
1293
if(zPageName && *zPageName){
1294
if( check_name(zPageName) ) return;
1295
}
1296
isSandbox = is_sandbox(zPageName);
1297
if( isSandbox ){
1298
if( !g.perm.RdWiki ){
1299
login_needed(g.anon.RdWiki);
1300
return;
1301
}
1302
found = 1;
1303
}else if( zPageName!=0 && zPageName[0]!=0){
1304
int rid = 0;
1305
if( !wiki_special_permission(zPageName) ){
1306
login_needed(0);
1307
return;
1308
}
1309
found = wiki_fetch_by_name(zPageName, 0, &rid, 0);
1310
if( (rid && !g.perm.RdWiki) || (!rid && !g.perm.NewWiki) ){
1311
login_needed(rid ? g.anon.RdWiki : g.anon.NewWiki);
1312
return;
1313
}
1314
}else{
1315
if( !g.perm.RdWiki ){
1316
login_needed(g.anon.RdWiki);
1317
return;
1318
}
1319
}
1320
style_set_current_feature("wiki");
1321
style_header("Wiki Editor");
1322
style_emit_noscript_for_js_page();
1323
1324
/* Status bar */
1325
CX("<div id='fossil-status-bar' "
1326
"title='Status message area. Double-click to clear them.'>"
1327
"Status messages will go here.</div>\n"
1328
/* will be moved into the tab container via JS */);
1329
1330
CX("<div id='wikiedit-edit-status'>"
1331
"<span class='name'></span>"
1332
"<span class='links'></span>"
1333
"</div>");
1334
1335
/* Main tab container... */
1336
CX("<div id='wikiedit-tabs' class='tab-container'>Loading...</div>");
1337
/* The .hidden class on the following tab elements is to help lessen
1338
the FOUC effect of the tabs before JS re-assembles them. */
1339
1340
/******* Page list *******/
1341
{
1342
CX("<div id='wikiedit-tab-pages' "
1343
"data-tab-parent='wikiedit-tabs' "
1344
"data-tab-label='Wiki Page List' "
1345
"class='hidden'"
1346
">");
1347
CX("<div>Loading wiki pages list...</div>");
1348
CX("</div>"/*#wikiedit-tab-pages*/);
1349
}
1350
1351
/******* Content tab *******/
1352
{
1353
CX("<div id='wikiedit-tab-content' "
1354
"data-tab-parent='wikiedit-tabs' "
1355
"data-tab-label='Editor' "
1356
"class='hidden'"
1357
">");
1358
CX("<div class='"
1359
"wikiedit-options flex-container flex-row child-gap-small'>");
1360
CX("<div class='input-with-label'>"
1361
"<label>Mime type</label>");
1362
mimetype_option_menu("text/x-markdown", "mimetype");
1363
CX("</div>");
1364
style_select_list_int("select-font-size",
1365
"editor_font_size", "Editor font size",
1366
NULL/*tooltip*/,
1367
100,
1368
"100%", 100, "125%", 125,
1369
"150%", 150, "175%", 175,
1370
"200%", 200, NULL);
1371
CX("<div class='input-with-label'>"
1372
/*will get moved around dynamically*/
1373
"<button class='wikiedit-save'>"
1374
"Save</button>"
1375
"<button class='wikiedit-save-close'>"
1376
"Save &amp; Close</button>"
1377
"<div class='help-buttonlet'>"
1378
"Save edits to this page and optionally return "
1379
"to the wiki page viewer."
1380
"</div>"
1381
"</div>" /*will get moved around dynamically*/);
1382
CX("<span class='save-button-slot'></span>");
1383
1384
CX("<div class='input-with-label'>"
1385
"<button class='wikiedit-content-reload' "
1386
">Discard &amp; Reload</button>"
1387
"<div class='help-buttonlet'>"
1388
"Reload the file from the server, discarding "
1389
"any local edits. To help avoid accidental loss of "
1390
"edits, it requires confirmation (a second click) within "
1391
"a few seconds or it will not reload."
1392
"</div>"
1393
"</div>");
1394
wikiedit_emit_toggle_preview();
1395
CX("</div>");
1396
CX("<div class='flex-container flex-column stretch'>");
1397
CX("<textarea name='content' id='wikiedit-content-editor' "
1398
"class='wikiedit' rows='25'>");
1399
CX("</textarea>");
1400
CX("</div>"/*textarea wrapper*/);
1401
CX("</div>"/*#tab-file-content*/);
1402
}
1403
/****** Preview tab ******/
1404
{
1405
CX("<div id='wikiedit-tab-preview' "
1406
"data-tab-parent='wikiedit-tabs' "
1407
"data-tab-label='Preview' "
1408
"class='hidden'"
1409
">");
1410
CX("<div class='wikiedit-options flex-container "
1411
"flex-row child-gap-small'>");
1412
CX("<button id='btn-preview-refresh' "
1413
"data-f-preview-from='wikiContent' "
1414
/* ^^^ fossil.page[methodName]() OR text source elem ID,
1415
** but we need a method in order to support clients swapping out
1416
** the text editor with their own. */
1417
"data-f-preview-via='_postPreview' "
1418
/* ^^^ fossil.page[methodName](content, callback) */
1419
"data-f-preview-to='_previewTo' "
1420
/* ^^^ dest elem ID or fossil.page[methodName]*/
1421
">Refresh</button>");
1422
/* Toggle auto-update of preview when the Preview tab is selected. */
1423
CX("<div class='input-with-label'>"
1424
"<input type='checkbox' value='1' "
1425
"id='cb-preview-autorefresh' checked>"
1426
"<label for='cb-preview-autorefresh'>Auto-refresh?</label>"
1427
"</div>");
1428
CX("<span class='save-button-slot'></span>");
1429
CX("</div>"/*.wikiedit-options*/);
1430
CX("<div id='wikiedit-tab-preview-wrapper'></div>");
1431
CX("</div>"/*#wikiedit-tab-preview*/);
1432
}
1433
1434
/****** Diff tab ******/
1435
{
1436
CX("<div id='wikiedit-tab-diff' "
1437
"data-tab-parent='wikiedit-tabs' "
1438
"data-tab-label='Diff' "
1439
"class='hidden'"
1440
">");
1441
1442
CX("<div class='wikiedit-options flex-container "
1443
"flex-row child-gap-small' "
1444
"id='wikiedit-tab-diff-buttons'>");
1445
CX("<div class='input-with-label'>"
1446
"<button class='sbs'>Side-by-side</button>"
1447
"<button class='unified'>Unified</button>"
1448
"</div>");
1449
CX("<span class='save-button-slot'></span>");
1450
CX("</div>");
1451
CX("<div id='wikiedit-tab-diff-wrapper'>"
1452
"Diffs will be shown here."
1453
"</div>");
1454
CX("</div>"/*#wikiedit-tab-diff*/);
1455
}
1456
1457
/****** The obligatory "Misc" tab ******/
1458
{
1459
CX("<div id='wikiedit-tab-misc' "
1460
"data-tab-parent='wikiedit-tabs' "
1461
"data-tab-label='Misc.' "
1462
"class='hidden'"
1463
">");
1464
CX("<fieldset id='attachment-wrapper'>");
1465
CX("<legend>Attachments</legend>");
1466
CX("<div>No attachments for the current page.</div>");
1467
CX("</fieldset>");
1468
CX("<h2>Wiki formatting rules</h2>");
1469
CX("<ul>");
1470
CX("<li><a href='%R/wiki_rules'>Fossil wiki format</a></li>");
1471
CX("<li><a href='%R/md_rules'>Markdown format</a></li>");
1472
CX("<li>Plain-text pages use no special formatting.</li>");
1473
CX("</ul>");
1474
CX("<h2>The \"Sandbox\" Page</h2>");
1475
CX("<p>The page named \"Sandbox\" is not a real wiki page. "
1476
"It provides a place where users may test out wiki syntax "
1477
"without having to actually save anything, nor pollute "
1478
"the repo with endless test runs. Any attempt to save the "
1479
"sandbox page will fail.</p>");
1480
CX("<h2>Wiki Name Rules</h2>");
1481
well_formed_wiki_name_rules();
1482
CX("</div>"/*#wikiedit-tab-save*/);
1483
}
1484
builtin_fossil_js_bundle_or("fetch", "dom", "tabs", "confirmer",
1485
"storage", "popupwidget", "copybutton",
1486
"pikchr", NULL);
1487
builtin_fossil_js_bundle_or("diff", NULL);
1488
builtin_request_js("fossil.page.wikiedit.js");
1489
builtin_fulfill_js_requests();
1490
/* Dynamically populate the editor... */
1491
style_script_begin(__FILE__,__LINE__);
1492
{
1493
/* Render the current page list to save us an XHR request
1494
during page initialization. This must be OUTSIDE of
1495
an onPageLoad() handler or else it does not get applied
1496
until after the wiki list widget is initialized. Similarly,
1497
it must come *after* window.fossil is initialized. */
1498
CX("\nfossil.page.initialPageList = ");
1499
wiki_render_page_list_json(1, 0);
1500
CX(";\n");
1501
}
1502
CX("fossil.onPageLoad(function(){\n");
1503
CX("const P = fossil.page;\n"
1504
"try{\n");
1505
if(!found && zPageName && *zPageName){
1506
/* For a new page, stick a dummy entry in the JS-side stash
1507
and "load" it from there. */
1508
CX("const winfo = {"
1509
"\"name\": %!j, \"mimetype\": %!j, "
1510
"\"type\": %!j, "
1511
"\"parent\": null, \"version\": null"
1512
"};\n",
1513
zPageName,
1514
zMimetype ? zMimetype : "text/x-markdown",
1515
wiki_page_type_name(zPageName));
1516
/* If the JS-side stash already has this page, load that
1517
copy from the stash, otherwise inject a new stash entry
1518
for it and load *that* one... */
1519
CX("if(!P.$stash.getWinfo(winfo)){"
1520
"P.$stash.updateWinfo(winfo,'');"
1521
"}\n");
1522
}
1523
if(zPageName && *zPageName){
1524
CX("P.loadPage(%!j);\n", zPageName);
1525
}
1526
CX("}catch(e){"
1527
"fossil.error(e); console.error('Exception:',e);"
1528
"}\n");
1529
CX("});\n"/*fossil.onPageLoad()*/);
1530
style_script_end();
1531
style_finish_page();
1532
}
1533
1534
/*
1535
** WEBPAGE: wikinew
1536
** URL /wikinew
1537
**
1538
** Prompt the user to enter the name of a new wiki page. Then redirect
1539
** to the wikiedit screen for that new page.
1540
*/
1541
void wikinew_page(void){
1542
const char *zName;
1543
const char *zMimetype;
1544
login_check_credentials();
1545
if( !g.perm.NewWiki ){
1546
login_needed(g.anon.NewWiki);
1547
return;
1548
}
1549
zName = PD("name","");
1550
zMimetype = wiki_filter_mimetypes(P("mimetype"));
1551
if( zName[0] && wiki_name_is_wellformed((const unsigned char *)zName) ){
1552
cgi_redirectf("wikiedit?name=%T&mimetype=%s", zName, zMimetype);
1553
}
1554
style_set_current_feature("wiki");
1555
style_header("Create A New Wiki Page");
1556
wiki_standard_submenu(W_ALL_BUT(W_NEW));
1557
@ <p>Rules for wiki page names:</p>
1558
well_formed_wiki_name_rules();
1559
form_begin(0, "%R/wikinew");
1560
@ <p>Name of new wiki page:
1561
@ <input style="width: 35;" type="text" name="name" value="%h(zName)"><br>
1562
@ %z(href("%R/markup_help"))Markup style</a>:
1563
mimetype_option_menu("text/x-markdown", "mimetype");
1564
@ <br><input type="submit" value="Create">
1565
@ </p></form>
1566
if( zName[0] ){
1567
@ <p><span class="wikiError">
1568
@ "%h(zName)" is not a valid wiki page name!</span></p>
1569
}
1570
style_finish_page();
1571
}
1572
1573
1574
/*
1575
** Append the wiki text for an remark to the end of the given BLOB.
1576
*/
1577
static void appendRemark(Blob *p, const char *zMimetype){
1578
char *zDate;
1579
const char *zUser;
1580
const char *zRemark;
1581
char *zId;
1582
1583
zDate = db_text(0, "SELECT datetime('now')");
1584
zRemark = PD("r","");
1585
zUser = PD("u",g.zLogin);
1586
if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
1587
zId = db_text(0, "SELECT lower(hex(randomblob(8)))");
1588
blob_appendf(p, "\n\n<hr><div id=\"%s\"><i>On %s UTC %h",
1589
zId, zDate, login_name());
1590
if( zUser[0] && fossil_strcmp(zUser,login_name()) ){
1591
blob_appendf(p, " (claiming to be %h)", zUser);
1592
}
1593
blob_appendf(p, " added:</i><br>\n%s</div id=\"%s\">", zRemark, zId);
1594
}else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
1595
blob_appendf(p, "\n\n------\n*On %s UTC %h", zDate, login_name());
1596
if( zUser[0] && fossil_strcmp(zUser,login_name()) ){
1597
blob_appendf(p, " (claiming to be %h)", zUser);
1598
}
1599
blob_appendf(p, " added:*\n\n%s\n", zRemark);
1600
}else{
1601
blob_appendf(p, "\n\n------------------------------------------------\n"
1602
"On %s UTC %s", zDate, login_name());
1603
if( zUser[0] && fossil_strcmp(zUser,login_name()) ){
1604
blob_appendf(p, " (claiming to be %s)", zUser);
1605
}
1606
blob_appendf(p, " added:\n\n%s\n", zRemark);
1607
}
1608
fossil_free(zDate);
1609
}
1610
1611
/*
1612
** WEBPAGE: wikiappend
1613
** URL: /wikiappend?name=PAGENAME&mimetype=MIMETYPE
1614
**
1615
** Append text to the end of a wiki page.
1616
*/
1617
void wikiappend_page(void){
1618
char *zTag;
1619
int rid = 0;
1620
const char *zPageName;
1621
const char *zUser;
1622
const char *zMimetype;
1623
int goodCaptcha = 1;
1624
const char *zFormat;
1625
Manifest *pWiki = 0;
1626
int isSandbox;
1627
1628
login_check_credentials();
1629
if( !g.perm.ApndWiki ){
1630
login_needed(g.anon.ApndWiki);
1631
return;
1632
}
1633
zPageName = PD("name","");
1634
zMimetype = wiki_filter_mimetypes(P("mimetype"));
1635
if( check_name(zPageName) ) return;
1636
isSandbox = is_sandbox(zPageName);
1637
if(!isSandbox){
1638
zTag = mprintf("wiki-%s", zPageName);
1639
rid = db_int(0,
1640
"SELECT rid FROM tagxref"
1641
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)"
1642
" ORDER BY mtime DESC", zTag
1643
);
1644
free(zTag);
1645
pWiki = rid ? manifest_get(rid, CFTYPE_WIKI, 0) : 0;
1646
if( !pWiki ){
1647
fossil_redirect_home();
1648
return;
1649
}
1650
zMimetype = wiki_filter_mimetypes(pWiki->zMimetype)
1651
/* see https://fossil-scm.org/forum/forumpost/0acfdaac80 */;
1652
}
1653
if( !isSandbox && P("submit")!=0 && P("r")!=0 && P("u")!=0
1654
&& (goodCaptcha = captcha_is_correct(0))
1655
&& cgi_csrf_safe(2)
1656
){
1657
char *zDate;
1658
Blob cksum;
1659
Blob body;
1660
Blob wiki;
1661
1662
blob_zero(&body);
1663
blob_append(&body, pWiki->zWiki, -1);
1664
blob_zero(&wiki);
1665
db_begin_transaction();
1666
zDate = date_in_standard_format("now");
1667
blob_appendf(&wiki, "D %s\n", zDate);
1668
blob_appendf(&wiki, "L %F\n", zPageName);
1669
if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")!=0 ){
1670
blob_appendf(&wiki, "N %s\n", zMimetype);
1671
}
1672
if( rid ){
1673
char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
1674
blob_appendf(&wiki, "P %s\n", zUuid);
1675
free(zUuid);
1676
}
1677
if( !login_is_nobody() ){
1678
blob_appendf(&wiki, "U %F\n", login_name());
1679
}
1680
appendRemark(&body, zMimetype);
1681
blob_appendf(&wiki, "W %d\n%s\n", blob_size(&body), blob_str(&body));
1682
md5sum_blob(&wiki, &cksum);
1683
blob_appendf(&wiki, "Z %b\n", &cksum);
1684
blob_reset(&cksum);
1685
wiki_put(&wiki, rid, wiki_need_moderation(0));
1686
db_end_transaction(0);
1687
manifest_destroy(pWiki);
1688
cgi_redirectf("wiki?name=%T", zPageName);
1689
return;
1690
}
1691
if( !isSandbox && P("cancel")!=0 ){
1692
manifest_destroy(pWiki);
1693
cgi_redirectf("wiki?name=%T", zPageName);
1694
return;
1695
}
1696
style_set_current_page("%T?name=%T", g.zPath, zPageName);
1697
style_set_current_feature("wiki");
1698
style_header("Append Comment To: %s", zPageName);
1699
if( !goodCaptcha ){
1700
@ <p class="generalError">Error: Incorrect security code.</p>
1701
}
1702
if( isSandbox ){
1703
@ <p class="generalError">Error: the Sandbox page may not
1704
@ be appended to.</p>
1705
}
1706
if( !isSandbox && P("preview")!=0 ){
1707
Blob preview;
1708
blob_zero(&preview);
1709
appendRemark(&preview, zMimetype);
1710
@ Preview:<hr>
1711
safe_html_context(DOCSRC_WIKI);
1712
wiki_render_by_mimetype(&preview, zMimetype);
1713
@ <hr>
1714
blob_reset(&preview);
1715
}
1716
zUser = PD("u", g.zLogin);
1717
form_begin(0, "%R/wikiappend");
1718
@ <input type="hidden" name="name" value="%h(zPageName)">
1719
@ <input type="hidden" name="mimetype" value="%h(zMimetype)">
1720
@ Your Name:
1721
@ <input type="text" name="u" size="20" value="%h(zUser)"><br>
1722
zFormat = mimetype_common_name(zMimetype);
1723
@ Comment to append (formatted as %s(zFormat)):<br>
1724
@ <textarea name="r" class="wikiedit" cols="80"
1725
@ rows="10" wrap="virtual">%h(PD("r",""))</textarea>
1726
@ <br>
1727
@ <input type="submit" name="preview" value="Preview Your Comment">
1728
@ <input type="submit" name="submit" value="Append Your Changes">
1729
@ <input type="submit" name="cancel" value="Cancel">
1730
captcha_generate(0);
1731
@ </form>
1732
manifest_destroy(pWiki);
1733
style_finish_page();
1734
}
1735
1736
/*
1737
** WEBPAGE: whistory
1738
** URL: /whistory?name=PAGENAME
1739
**
1740
** Additional parameters:
1741
**
1742
** showid Show RID values
1743
**
1744
** Show the complete change history for a single wiki page.
1745
*/
1746
void whistory_page(void){
1747
Stmt q;
1748
const char *zPageName;
1749
double rNow;
1750
int showRid;
1751
char zAuthor[64];
1752
login_check_credentials();
1753
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1754
zPageName = PD("name","");
1755
style_set_current_feature("wiki");
1756
style_header("History Of %s", zPageName);
1757
showRid = P("showid")!=0;
1758
db_prepare(&q,
1759
"SELECT"
1760
" event.mtime,"
1761
" blob.uuid,"
1762
" coalesce(event.euser,event.user),"
1763
" event.objid,"
1764
" datetime(event.mtime)"
1765
" FROM event, blob, tag, tagxref"
1766
" WHERE event.type='w' AND blob.rid=event.objid"
1767
" AND tag.tagname='wiki-%q'"
1768
" AND tagxref.tagid=tag.tagid AND tagxref.srcid=event.objid"
1769
" ORDER BY event.mtime DESC",
1770
zPageName
1771
);
1772
@ <h2>History of <a href="%R/wiki?name=%T(zPageName)">%h(zPageName)</a></h2>
1773
form_begin( "id='wh-form'", "%R/wdiff" );
1774
@ <input id="wh-pid" name="pid" type="radio" hidden>
1775
@ <input id="wh-id" name="id" type="hidden">
1776
@ </form>
1777
@ <style> .wh-clickable { cursor: pointer; } </style>
1778
@ <div class="brlist">
1779
@ <table>
1780
@ <thead><tr>
1781
@ <th>Age</th>
1782
@ <th>Hash</th>
1783
@ <th><span title="Baseline from which diffs are computed (click to unset)"
1784
@ id="wh-cleaner" class="wh-clickable">&#9875;</span></th>
1785
@ <th>User<span hidden class="wh-clickable"
1786
@ id="wh-collapser">&emsp;&#9842;</span></th>
1787
if( showRid ){
1788
@ <th>RID</th>
1789
}
1790
@ <th>&nbsp;</th>
1791
@ </tr></thead><tbody>
1792
rNow = db_double(0.0, "SELECT julianday('now')");
1793
memset( zAuthor, 0, sizeof(zAuthor) );
1794
while( db_step(&q)==SQLITE_ROW ){
1795
double rMtime = db_column_double(&q, 0);
1796
const char *zUuid = db_column_text(&q, 1);
1797
const char *zUser = db_column_text(&q, 2);
1798
int wrid = db_column_int(&q, 3);
1799
const char *zWhen = db_column_text(&q, 4);
1800
/* sqlite3_int64 iMtime = (sqlite3_int64)(rMtime*86400.0); */
1801
char *zAge = human_readable_age(rNow - rMtime);
1802
if( strncmp( zAuthor, zUser, sizeof(zAuthor) - 1 ) == 0 ) {
1803
@ <tr class="wh-intermediate" title="%s(zWhen)">
1804
}
1805
else {
1806
strncpy( zAuthor, zUser, sizeof(zAuthor) - 1 );
1807
@ <tr class="wh-major" title="%s(zWhen)">
1808
}
1809
/* @ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td> */
1810
@ <td>%s(zAge)</td>
1811
fossil_free(zAge);
1812
@ <td>%z(href("%R/info/%s",zUuid))%S(zUuid)</a></td>
1813
@ <td><input disabled type="radio" name="baseline" value="%S(zUuid)"/></td>
1814
@ <td>%h(zUser)<span class="wh-iterations" hidden></td>
1815
if( showRid ){
1816
@ <td>%z(href("%R/artifact/%S",zUuid))%d(wrid)</a></td>
1817
}
1818
@ <td>%z(chref("wh-difflink","%R/wdiff?id=%S",zUuid))diff</a></td>
1819
@ </tr>
1820
}
1821
@ </tbody></table></div>
1822
db_finalize(&q);
1823
builtin_request_js("fossil.page.whistory.js");
1824
/* style_table_sorter(); */
1825
style_finish_page();
1826
}
1827
1828
/*
1829
** WEBPAGE: wdiff
1830
**
1831
** Show the changes to a wiki page.
1832
**
1833
** Query parameters:
1834
**
1835
** id=HASH Hash prefix for the child version to be diffed.
1836
** rid=INTEGER RecordID for the child version
1837
** pid=HASH Hash prefix for the parent.
1838
**
1839
** The "id" query parameter is required. "pid" is optional. If "pid"
1840
** is omitted, then the diff is against the first parent of the child.
1841
*/
1842
void wdiff_page(void){
1843
const char *zId;
1844
const char *zIdFull;
1845
const char *zPid;
1846
Manifest *pW1, *pW2 = 0;
1847
int rid1, rid2, nextRid;
1848
Blob w1, w2, d;
1849
DiffConfig DCfg;
1850
1851
login_check_credentials();
1852
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1853
zId = P("id");
1854
if( zId==0 ){
1855
rid1 = atoi(PD("rid","0"));
1856
}else{
1857
rid1 = name_to_typed_rid(zId, "w");
1858
}
1859
zIdFull = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid1);
1860
if( zIdFull==0 ){
1861
if( zId ){
1862
webpage_notfound_error("No such wiki page: \"%s\"", zId);
1863
}else{
1864
webpage_notfound_error("No such wiki page: %d", rid1);
1865
}
1866
return;
1867
}
1868
zId = zIdFull;
1869
pW1 = manifest_get(rid1, CFTYPE_WIKI, 0);
1870
if( pW1==0 ) fossil_redirect_home();
1871
blob_init(&w1, pW1->zWiki, -1);
1872
zPid = P("pid");
1873
if( ( zPid==0 || zPid[0] == 0 ) && pW1->nParent ){
1874
zPid = pW1->azParent[0];
1875
}
1876
cgi_check_for_malice();
1877
if( zPid && zPid[0] != 0 ){
1878
char *zDate;
1879
rid2 = name_to_typed_rid(zPid, "w");
1880
pW2 = manifest_get(rid2, CFTYPE_WIKI, 0);
1881
blob_init(&w2, pW2->zWiki, -1);
1882
@ <h2>Changes to \
1883
@ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>" \
1884
zDate = db_text(0, "SELECT datetime(%.16g,toLocal())",pW2->rDate);
1885
@ between %z(href("%R/info/%s",zPid))%z(zDate)</a> \
1886
zDate = db_text(0, "SELECT datetime(%.16g,toLocal())",pW1->rDate);
1887
@ and %z(href("%R/info/%s",zId))%z(zDate)</a></h2>
1888
style_submenu_element("Previous", "%R/wdiff?id=%S", zPid);
1889
}else{
1890
blob_zero(&w2);
1891
@ <h2>Initial version of \
1892
@ "%z(href("%R/whistory?name=%s",pW1->zWikiTitle))%h(pW1->zWikiTitle)</a>"\
1893
@ </h2>
1894
}
1895
nextRid = wiki_next(wiki_tagid(pW1->zWikiTitle),pW1->rDate);
1896
if( nextRid ){
1897
style_submenu_element("Next", "%R/wdiff?rid=%d", nextRid);
1898
}
1899
style_set_current_feature("wiki");
1900
style_header("Changes To %s", pW1->zWikiTitle);
1901
blob_zero(&d);
1902
construct_diff_flags(1, &DCfg);
1903
DCfg.diffFlags |= DIFF_HTML | DIFF_LINENO;
1904
text_diff(&w2, &w1, &d, &DCfg);
1905
@ %s(blob_str(&d))
1906
manifest_destroy(pW1);
1907
manifest_destroy(pW2);
1908
style_finish_page();
1909
}
1910
1911
/*
1912
** A query that returns information about all wiki pages.
1913
**
1914
** wname Name of the wiki page
1915
** wsort Sort names by this label
1916
** wrid rid of the most recent version of the page
1917
** wmtime time most recent version was created
1918
** wcnt Number of versions of this wiki page
1919
**
1920
** The wrid value is zero for deleted wiki pages.
1921
*/
1922
static const char listAllWikiPages[] =
1923
@ SELECT
1924
@ substr(tag.tagname, 6) AS wname,
1925
@ lower(substr(tag.tagname, 6)) AS sortname,
1926
@ tagxref.value+0 AS wrid,
1927
@ max(tagxref.mtime) AS wmtime,
1928
@ count(*) AS wcnt
1929
@ FROM
1930
@ tag,
1931
@ tagxref
1932
@ WHERE
1933
@ tag.tagname GLOB 'wiki-*'
1934
@ AND tagxref.tagid=tag.tagid
1935
@ AND TYPEOF(wrid)='integer' -- only wiki- tags which are wiki pages
1936
@ GROUP BY 1
1937
@ ORDER BY 2;
1938
;
1939
1940
/*
1941
** WEBPAGE: wcontent
1942
**
1943
** all=1 Show deleted pages
1944
** showid Show rid values for each page.
1945
**
1946
** List all available wiki pages with date created and last modified.
1947
*/
1948
void wcontent_page(void){
1949
Stmt q;
1950
double rNow;
1951
int showAll = P("all")!=0;
1952
int showRid = P("showid")!=0;
1953
int showCkBr;
1954
1955
login_check_credentials();
1956
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
1957
style_set_current_feature("wiki");
1958
style_header("Available Wiki Pages");
1959
if( showAll ){
1960
style_submenu_element("Active", "%R/wcontent");
1961
}else{
1962
style_submenu_element("All", "%R/wcontent?all=1");
1963
}
1964
cgi_check_for_malice();
1965
showCkBr = db_exists(
1966
"SELECT tag.tagname AS tn FROM tag JOIN tagxref USING(tagid) "
1967
"WHERE ( tn GLOB 'wiki-checkin/*' OR tn GLOB 'wiki-branch/*' OR "
1968
" tn GLOB 'wiki-tag/*' OR tn GLOB 'wiki-ticket/*' ) "
1969
" AND TYPEOF(tagxref.value+0)='integer'" );
1970
if( showCkBr ){
1971
showCkBr = P("showckbr")!=0;
1972
style_submenu_checkbox("showckbr", "Show associated wikis", 0, 0);
1973
}
1974
wiki_standard_submenu(W_ALL_BUT(W_LIST));
1975
db_prepare(&q, listAllWikiPages/*works-like:""*/);
1976
@ <div class="brlist">
1977
@ <table class='sortable' data-column-types='tKN' data-init-sort='1'>
1978
@ <thead><tr>
1979
@ <th>Name</th>
1980
@ <th>Last Change</th>
1981
@ <th>Versions</th>
1982
if( showRid ){
1983
@ <th>RID</th>
1984
}
1985
@ </tr></thead><tbody>
1986
rNow = db_double(0.0, "SELECT julianday('now')");
1987
while( db_step(&q)==SQLITE_ROW ){
1988
const char *zWName = db_column_text(&q, 0);
1989
const char *zSort = db_column_text(&q, 1);
1990
int wrid = db_column_int(&q, 2);
1991
double rWmtime = db_column_double(&q, 3);
1992
sqlite3_int64 iMtime = (sqlite3_int64)(rWmtime*86400.0);
1993
char *zAge;
1994
int wcnt = db_column_int(&q, 4);
1995
char *zWDisplayName;
1996
1997
if( !showCkBr &&
1998
(has_prefix("checkin/", zWName) ||
1999
has_prefix("branch/", zWName) ||
2000
has_prefix("tag/", zWName) ||
2001
has_prefix("ticket/", zWName) )){
2002
continue;
2003
}
2004
if( has_prefix("checkin/",zWName) || has_prefix("ticket/",zWName) ){
2005
zWDisplayName = mprintf("%.25s...", zWName);
2006
}else{
2007
zWDisplayName = fossil_strdup(zWName);
2008
}
2009
if( wrid==0 ){
2010
if( !showAll ) continue;
2011
@ <tr><td data-sortkey="%h(zSort)">\
2012
@ %z(href("%R/whistory?name=%T",zWName))<s>%h(zWDisplayName)</s></a></td>
2013
}else{
2014
@ <tr><td data-sortkey="%h(zSort)">\
2015
@ %z(href("%R/wiki?name=%T&p",zWName))%h(zWDisplayName)</a></td>
2016
}
2017
zAge = human_readable_age(rNow - rWmtime);
2018
@ <td data-sortkey="%016llx(iMtime)">%s(zAge)</td>
2019
fossil_free(zAge);
2020
@ <td>%z(href("%R/whistory?name=%T",zWName))%d(wcnt)</a></td>
2021
if( showRid ){
2022
@ <td>%d(wrid)</td>
2023
}
2024
@ </tr>
2025
fossil_free(zWDisplayName);
2026
}
2027
@ </tbody></table></div>
2028
db_finalize(&q);
2029
style_table_sorter();
2030
style_finish_page();
2031
}
2032
2033
/*
2034
** WEBPAGE: wfind
2035
**
2036
** URL: /wfind?title=TITLE
2037
** List all wiki pages whose titles contain the search text
2038
*/
2039
void wfind_page(void){
2040
Stmt q;
2041
const char *zTitle;
2042
login_check_credentials();
2043
if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; }
2044
zTitle = PD("title","*");
2045
cgi_check_for_malice();
2046
style_set_current_feature("wiki");
2047
style_header("Wiki Pages Found");
2048
@ <ul>
2049
db_prepare(&q,
2050
"SELECT substr(tagname, 6, 1000) FROM tag WHERE tagname like 'wiki-%%%q%%'"
2051
" ORDER BY lower(tagname) /*sort*/" ,
2052
zTitle);
2053
while( db_step(&q)==SQLITE_ROW ){
2054
const char *zName = db_column_text(&q, 0);
2055
@ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li>
2056
}
2057
db_finalize(&q);
2058
@ </ul>
2059
style_finish_page();
2060
}
2061
2062
/*
2063
** Add a new wiki page to the repository. The page name is
2064
** given by the zPageName parameter. rid must be zero to create
2065
** a new page otherwise the page identified by rid is updated.
2066
**
2067
** The content of the new page is given by the blob pContent.
2068
**
2069
** zMimeType specifies the N-card for the wiki page. If it is 0,
2070
** empty, or "text/x-fossil-wiki" (the default format) then it is
2071
** ignored.
2072
*/
2073
int wiki_cmd_commit(const char *zPageName, int rid, Blob *pContent,
2074
const char *zMimeType, int localUser){
2075
Blob wiki; /* Wiki page content */
2076
Blob cksum; /* wiki checksum */
2077
char *zDate; /* timestamp */
2078
2079
blob_zero(&wiki);
2080
zDate = date_in_standard_format("now");
2081
blob_appendf(&wiki, "D %s\n", zDate);
2082
free(zDate);
2083
blob_appendf(&wiki, "L %F\n", zPageName );
2084
if( zMimeType && *zMimeType
2085
&& 0!=fossil_strcmp(zMimeType,"text/x-fossil-wiki") ){
2086
blob_appendf(&wiki, "N %F\n", zMimeType);
2087
}
2088
if( rid ){
2089
char *zUuid = rid_to_uuid(rid);
2090
blob_appendf(&wiki, "P %z\n", zUuid);
2091
}
2092
user_select();
2093
if( !login_is_nobody() ){
2094
blob_appendf(&wiki, "U %F\n", login_name());
2095
}
2096
blob_appendf( &wiki, "W %d\n%s\n", blob_size(pContent),
2097
blob_str(pContent) );
2098
md5sum_blob(&wiki, &cksum);
2099
blob_appendf(&wiki, "Z %b\n", &cksum);
2100
blob_reset(&cksum);
2101
db_begin_transaction();
2102
wiki_put(&wiki, 0, wiki_need_moderation(localUser));
2103
db_end_transaction(0);
2104
return 1;
2105
}
2106
2107
/*
2108
** Determine the rid for a tech note given either its id, its timestamp,
2109
** or its tag. Returns 0 if there is no such item and -1 if the details
2110
** are ambiguous and could refer to multiple items.
2111
*/
2112
int wiki_technote_to_rid(const char *zETime) {
2113
int rid=0; /* Artifact ID of the tech note */
2114
int nETime = strlen(zETime);
2115
Stmt q;
2116
if( nETime>=4 && nETime<=HNAME_MAX && validate16(zETime, nETime) ){
2117
char zUuid[HNAME_MAX+1];
2118
memcpy(zUuid, zETime, nETime+1);
2119
canonical16(zUuid, nETime);
2120
db_prepare(&q,
2121
"SELECT e.objid"
2122
" FROM event e, tag t"
2123
" WHERE e.type='e' AND e.tagid IS NOT NULL AND t.tagid=e.tagid"
2124
" AND t.tagname GLOB 'event-%q*'",
2125
zUuid
2126
);
2127
if( db_step(&q)==SQLITE_ROW ){
2128
rid = db_column_int(&q, 0);
2129
if( db_step(&q)==SQLITE_ROW ) rid = -1;
2130
}
2131
db_finalize(&q);
2132
}
2133
if (!rid) {
2134
if (strlen(zETime)>4) {
2135
rid = db_int(0, "SELECT objid"
2136
" FROM event"
2137
" WHERE datetime(mtime)=datetime('%q')"
2138
" AND type='e'"
2139
" AND tagid IS NOT NULL"
2140
" ORDER BY objid DESC LIMIT 1",
2141
zETime);
2142
}
2143
}
2144
if( !rid ) {
2145
/*
2146
** At present, technote tags are prefixed with 'sym-', which shouldn't
2147
** be the case, so we check for both with and without the prefix until
2148
** such time as tags have the errant prefix dropped.
2149
*/
2150
rid = db_int(0, "SELECT e.objid"
2151
" FROM event e"
2152
" WHERE e.type='e'"
2153
" AND e.tagid IS NOT NULL"
2154
" AND (e.objid IN"
2155
" (SELECT rid FROM tagxref"
2156
" WHERE tagid=(SELECT tagid FROM tag"
2157
" WHERE tagname GLOB '%q'))"
2158
" OR (e.objid IN"
2159
" (SELECT rid FROM tagxref"
2160
" WHERE tagid=(SELECT tagid FROM tag"
2161
" WHERE tagname GLOB 'sym-%q')))"
2162
" )"
2163
" ORDER BY e.mtime DESC LIMIT 1",
2164
zETime, zETime);
2165
}
2166
return rid;
2167
}
2168
2169
/*
2170
** COMMAND: wiki*
2171
**
2172
** Usage: %fossil wiki (export|create|commit|list) WikiName
2173
**
2174
** Run various subcommands to work with wiki entries or tech notes.
2175
**
2176
** > fossil wiki export ?OPTIONS? PAGENAME ?FILE?
2177
** > fossil wiki export ?OPTIONS? -t|--technote DATETIME|TECHNOTE-ID|TAG ?FILE?
2178
**
2179
** Sends the latest version of either a wiki page or of a tech
2180
** note to the given file or standard output. A filename of "-"
2181
** writes the output to standard output. The directory parts of
2182
** the output filename are created if needed.
2183
** If PAGENAME is provided, the named wiki page will be output.
2184
**
2185
** Options:
2186
** -t|--technote DATETIME|TECHNOTE-ID|TAG
2187
** Specifies that a technote, rather than a wiki page,
2188
** will be exported. If DATETIME is used, the most
2189
** recently modified tech note with that DATETIME will
2190
** output. If TAG is used, the most recently modified
2191
** tech note with that TAG will be output.
2192
** -h|--html The body (only) is rendered in HTML form, without
2193
** any page header/foot or HTML/BODY tag wrappers.
2194
** -H|--HTML Works like -h|-html but wraps the output in
2195
** <html><body>...</body></html>.
2196
** -p|--pre If -h|-H is used and the page or technote has
2197
** the text/plain mimetype, its HTML-escaped output
2198
** will be wrapped in <pre>...</pre>.
2199
**
2200
** > fossil wiki (create|commit) (PAGENAME | TECHNOTE-COMMENT) ?FILE? ?OPTIONS?
2201
**
2202
** Create a new or commit changes to an existing wiki page or
2203
** technote from FILE or from standard input. PAGENAME is the
2204
** name of the wiki entry. TECHNOTE-COMMENT is the timeline comment of
2205
** the technote.
2206
**
2207
** Options:
2208
** -M|--mimetype TEXT-FORMAT The mime type of the update.
2209
** Defaults to the type used by
2210
** the previous version of the
2211
** page, or text/x-fossil-wiki.
2212
** Valid values are: text/x-fossil-wiki,
2213
** text/x-markdown and text/plain. fossil,
2214
** markdown or plain can be specified as
2215
** synonyms of these values.
2216
** -t|--technote DATETIME Specifies the timestamp of
2217
** the technote to be created or
2218
** updated. The timestamp specifies when
2219
** this technote appears in the timeline
2220
** and is its permanent handle although
2221
** it may not be unique. When updating
2222
** a technote the most recently modified
2223
** tech note with the specified timestamp
2224
** will be updated.
2225
** -t|--technote TECHNOTE-ID Specifies the technote to be
2226
** updated by its technote id, which is
2227
** its UUID.
2228
** --technote-tags TAGS The set of tags for a technote.
2229
** --technote-bgcolor COLOR The color used for the technote
2230
** on the timeline.
2231
**
2232
** > fossil wiki list ?OPTIONS?
2233
** > fossil wiki ls ?OPTIONS?
2234
**
2235
** Lists all wiki entries, one per line, ordered
2236
** case-insensitively by name. Wiki pages associated with
2237
** check-ins and branches are NOT shown, unless -a is given.
2238
**
2239
** Options:
2240
** --all Include "deleted" pages in output.
2241
** By default deleted pages are elided.
2242
** -t|--technote Technotes will be listed instead of
2243
** pages. The technotes will be in order
2244
** of timestamp with the most recent
2245
** first.
2246
** -a|--show-associated Show wiki pages associated with
2247
** check-ins and branches.
2248
** -s|--show-technote-ids The id of the tech note will be listed
2249
** alongside the timestamp. The tech note
2250
** id will be the first word on each line.
2251
** This option only applies if the
2252
** --technote option is also specified.
2253
**
2254
** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
2255
** year-month-day form, it may be truncated, the "T" may be replaced by
2256
** a space, and it may also name a timezone offset from UTC as "-HH:MM"
2257
** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z"
2258
** means UTC.
2259
**
2260
** The "Sandbox" wiki pseudo-page is a special case. Its name is
2261
** checked case-insensitively and either "create" or "commit" may be
2262
** used to update its contents.
2263
*/
2264
void wiki_cmd(void){
2265
int n;
2266
int isSandbox = 0; /* true if dealing with sandbox pseudo-page */
2267
const int showAll = find_option("all", 0, 0)!=0;
2268
2269
db_find_and_open_repository(0, 0);
2270
if( g.argc<3 ){
2271
goto wiki_cmd_usage;
2272
}
2273
n = strlen(g.argv[2]);
2274
if( n==0 ){
2275
goto wiki_cmd_usage;
2276
}
2277
2278
if( strncmp(g.argv[2],"export",n)==0 ){
2279
const char *zPageName = 0; /* Name of the wiki page to export */
2280
const char *zFile; /* Name of the output file (0=stdout) */
2281
const char *zETime; /* The name of the technote to export */
2282
int rid = 0; /* Artifact ID of the wiki page */
2283
int i; /* Loop counter */
2284
char *zBody = 0; /* Wiki page content */
2285
Blob body = empty_blob; /* Wiki page content */
2286
Manifest *pWiki = 0; /* Parsed wiki page content */
2287
int fHtml = 0; /* Export in HTML form */
2288
FILE * pFile = 0; /* Output file */
2289
int fPre = 0; /* Indicates that -h|-H should be
2290
** wrapped in <pre>...</pre> if pWiki
2291
** has the text/plain mimetype. */
2292
fHtml = find_option("HTML","H",0)!=0
2293
? 2
2294
: (find_option("html","h",0)!=0 ? 1 : 0)
2295
/* 1 == -html, 2 == -HTML */;
2296
fPre = fHtml==0 ? 0 : find_option("pre","p",0)!=0;
2297
zETime = find_option("technote","t",1);
2298
verify_all_options();
2299
if( !zETime ){
2300
if( (g.argc!=4) && (g.argc!=5) ){
2301
usage("export ?-html? PAGENAME ?FILE?");
2302
}
2303
zPageName = g.argv[3];
2304
isSandbox = is_sandbox(zPageName);
2305
if(isSandbox){
2306
zBody = db_get("sandbox", 0);
2307
}else{
2308
wiki_fetch_by_name(zPageName, 0, &rid, &pWiki);
2309
if(pWiki){
2310
zBody = pWiki->zWiki;
2311
}
2312
}
2313
if( zBody==0 ){
2314
fossil_fatal("wiki page [%s] not found",zPageName);
2315
}
2316
zFile = (g.argc==4) ? "-" : g.argv[4];
2317
}else{
2318
if( (g.argc!=3) && (g.argc!=4) ){
2319
usage("export ?-html? ?FILE? --technote "
2320
"DATETIME|TECHNOTE-ID");
2321
}
2322
rid = wiki_technote_to_rid(zETime);
2323
if ( rid==-1 ){
2324
fossil_fatal("ambiguous tech note id: %s", zETime);
2325
}
2326
if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){
2327
zBody = pWiki->zWiki;
2328
}
2329
if( zBody==0 ){
2330
fossil_fatal("technote [%s] not found",zETime);
2331
}
2332
zFile = (g.argc==3) ? "-" : g.argv[3];
2333
}
2334
for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){}
2335
zBody[i] = 0;
2336
blob_init(&body, zBody, -1);
2337
if(fHtml==0){
2338
blob_append(&body, "\n", 1);
2339
}else{
2340
Blob html = empty_blob; /* HTML-ized content */
2341
const char * zMimetype = isSandbox
2342
? db_get("sandbox-mimetype", "text/x-fossil-wiki")
2343
: wiki_filter_mimetypes(pWiki->zMimetype);
2344
if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
2345
wiki_convert(&body,&html,0);
2346
}else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
2347
markdown_to_html(&body,0,&html);
2348
safe_html_context(DOCSRC_WIKI);
2349
safe_html(&html);
2350
}else if( fossil_strcmp(zMimetype, "text/plain")==0 ){
2351
htmlize_to_blob(&html,zBody,i);
2352
}else{
2353
fossil_fatal("Unsupported MIME type '%s' for wiki page '%s'.",
2354
zMimetype, pWiki ? pWiki->zWikiTitle : zPageName );
2355
}
2356
blob_reset(&body);
2357
body = html /* transfer memory */;
2358
}
2359
pFile = fossil_fopen_for_output(zFile);
2360
if(fHtml==2){
2361
fwrite("<html><body>", 1, 12, pFile);
2362
}
2363
if(fPre!=0){
2364
fwrite("<pre>", 1, 5, pFile);
2365
}
2366
fwrite(blob_buffer(&body), 1, blob_size(&body), pFile);
2367
if(fPre!=0){
2368
fwrite("</pre>", 1, 6, pFile);
2369
}
2370
if(fHtml==2){
2371
fwrite("</body></html>\n", 1, 15, pFile);
2372
}
2373
fossil_fclose(pFile);
2374
blob_reset(&body);
2375
manifest_destroy(pWiki);
2376
return;
2377
}else if( strncmp(g.argv[2],"commit",n)==0
2378
|| strncmp(g.argv[2],"create",n)==0 ){
2379
const char *zPageName; /* page name */
2380
Blob content; /* Input content */
2381
int rid = 0;
2382
Manifest *pWiki = 0; /* Parsed wiki page content */
2383
const int isCreate = 'r'==g.argv[2][1] /* else "commit" */;
2384
const char *zMimeType = find_option("mimetype", "M", 1);
2385
const char *zETime = find_option("technote", "t", 1);
2386
const char *zTags = find_option("technote-tags", NULL, 1);
2387
const char *zClr = find_option("technote-bgcolor", NULL, 1);
2388
verify_all_options();
2389
if( g.argc!=4 && g.argc!=5 ){
2390
usage("commit|create PAGENAME ?FILE? [--mimetype TEXT-FORMAT]"
2391
" [--technote DATETIME] [--technote-tags TAGS]"
2392
" [--technote-bgcolor COLOR]");
2393
}
2394
zPageName = g.argv[3];
2395
if( g.argc==4 ){
2396
blob_read_from_channel(&content, stdin, -1);
2397
}else{
2398
blob_read_from_file(&content, g.argv[4], ExtFILE);
2399
}
2400
isSandbox = is_sandbox(zPageName);
2401
if ( !zETime ){
2402
if( !isSandbox ){
2403
wiki_fetch_by_name(zPageName, 0, &rid, &pWiki);
2404
}
2405
}else{
2406
rid = wiki_technote_to_rid(zETime);
2407
if( rid>0 ){
2408
pWiki = manifest_get(rid, CFTYPE_EVENT, 0);
2409
}
2410
}
2411
if( !zMimeType || !*zMimeType ){
2412
/* Try to deduce the mimetype based on the prior version. */
2413
if(isSandbox){
2414
zMimeType =
2415
wiki_filter_mimetypes(db_get("sandbox-mimetype",
2416
"text/x-fossil-wiki"));
2417
}else if( pWiki!=0 && (pWiki->zMimetype && *pWiki->zMimetype) ){
2418
zMimeType = pWiki->zMimetype;
2419
}
2420
}else{
2421
zMimeType = wiki_filter_mimetypes(zMimeType);
2422
}
2423
if( isCreate && rid>0 ){
2424
if ( !zETime ){
2425
fossil_fatal("wiki page %s already exists", zPageName);
2426
}else{
2427
/* Creating a tech note with same timestamp is permitted
2428
and should create a new tech note */
2429
rid = 0;
2430
}
2431
}else if( !isCreate && rid==0 && isSandbox==0 ){
2432
if ( !zETime ){
2433
fossil_fatal("no such wiki page: %s", zPageName);
2434
}else{
2435
fossil_fatal("no such tech note: %s", zETime);
2436
}
2437
}
2438
2439
if( !zETime ){
2440
if(isSandbox){
2441
db_set("sandbox",blob_str(&content),0);
2442
db_set("sandbox-mimetype",zMimeType,0);
2443
fossil_print("Updated sandbox pseudo-page.\n");
2444
}else{
2445
wiki_cmd_commit(zPageName, rid, &content, zMimeType, 1);
2446
if( g.argv[2][1]=='r' ){
2447
fossil_print("Created new wiki page %s.\n", zPageName);
2448
}else{
2449
fossil_print("Updated wiki page %s.\n", zPageName);
2450
}
2451
}
2452
}else{
2453
if( rid != -1 ){
2454
char *zMETime; /* Normalized, mutable version of zETime */
2455
zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))",
2456
zETime);
2457
event_cmd_commit(zMETime, rid, &content, zMimeType, zPageName,
2458
zTags, zClr);
2459
if( g.argv[2][1]=='r' ){
2460
fossil_print("Created new tech note %s.\n", zMETime);
2461
}else{
2462
fossil_print("Updated tech note %s.\n", zMETime);
2463
}
2464
free(zMETime);
2465
}else{
2466
fossil_fatal("ambiguous tech note id: %s", zETime);
2467
}
2468
}
2469
manifest_destroy(pWiki);
2470
blob_reset(&content);
2471
}else if( strncmp(g.argv[2],"delete",n)==0 ){
2472
if( g.argc!=4 ){
2473
usage("delete PAGENAME");
2474
}
2475
fossil_fatal("delete not yet implemented.");
2476
}else if(( strncmp(g.argv[2],"list",n)==0 )
2477
|| ( strncmp(g.argv[2],"ls",n)==0 )){
2478
Stmt q;
2479
const int fTechnote = find_option("technote","t",0)!=0;
2480
const int showIds = find_option("show-technote-ids","s",0)!=0;
2481
const int showCkBr = find_option("show-associated","a",0)!=0;
2482
verify_all_options();
2483
if (fTechnote==0){
2484
db_prepare(&q, listAllWikiPages/*works-like:""*/);
2485
}else{
2486
db_prepare(&q,
2487
"SELECT datetime(e.mtime), substr(t.tagname,7), e.objid"
2488
" FROM event e, tag t"
2489
" WHERE e.type='e'"
2490
" AND e.tagid IS NOT NULL"
2491
" AND t.tagid=e.tagid"
2492
" ORDER BY e.mtime DESC /*sort*/"
2493
);
2494
}
2495
while( db_step(&q)==SQLITE_ROW ){
2496
const char *zName = db_column_text(&q, 0);
2497
const int wrid = db_column_int(&q, 2);
2498
if(!showAll && !wrid){
2499
continue;
2500
}
2501
if( !showCkBr &&
2502
(has_prefix("checkin/", zName) ||
2503
has_prefix("branch/", zName) ||
2504
has_prefix("tag/", zName) ||
2505
has_prefix("ticket/", zName) ) ){
2506
continue;
2507
}
2508
if( showIds ){
2509
const char *zUuid = db_column_text(&q, 1);
2510
fossil_print("%s ",zUuid);
2511
}
2512
fossil_print( "%s\n",zName);
2513
}
2514
db_finalize(&q);
2515
}else{
2516
goto wiki_cmd_usage;
2517
}
2518
return;
2519
2520
wiki_cmd_usage:
2521
usage("export|create|commit|list ...");
2522
}
2523
2524
/*
2525
** Allowed flags for wiki_render_associated
2526
*/
2527
#if INTERFACE
2528
#define WIKIASSOC_FULL_TITLE 0x00001 /* Full title */
2529
#define WIKIASSOC_MENU_READ 0x00002 /* Add submenu link to read wiki */
2530
#define WIKIASSOC_MENU_WRITE 0x00004 /* Add submenu link to add wiki */
2531
#define WIKIASSOC_ALL 0x00007 /* All of the above */
2532
#endif
2533
2534
/*
2535
** Show the default Section label for an associated wiki page.
2536
*/
2537
static void wiki_section_label(
2538
const char *zPrefix, /* "branch", "tag", or "checkin" */
2539
const char *zName, /* Name of the object */
2540
unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
2541
){
2542
if( (mFlags & WIKIASSOC_FULL_TITLE)==0 ){
2543
@ <div class="section accordion">About</div>
2544
}else if( zPrefix[0]=='c' ){ /* checkin/... */
2545
@ <div class="section accordion">About check-in %.20h(zName)</div>
2546
}else{
2547
@ <div class="section accordion">About %s(zPrefix) %h(zName)</div>
2548
}
2549
}
2550
2551
/*
2552
** Add an "Wiki" button in a submenu that links to the read-wiki page.
2553
*/
2554
static void wiki_submenu_to_read_wiki(
2555
const char *zPrefix, /* "branch", "tag", or "checkin" */
2556
const char *zName, /* Name of the object */
2557
unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
2558
){
2559
if( g.perm.RdWiki && (mFlags & WIKIASSOC_MENU_READ)!=0
2560
&& 0!=fossil_strcmp("branch", zPrefix)
2561
/* ^^^ https://fossil-scm.org/forum/forumpost/ff453de2f30791dd */
2562
){
2563
style_submenu_element("Wiki", "%R/wiki?name=%s/%t", zPrefix, zName);
2564
}
2565
}
2566
2567
/*
2568
** Add an "Edit Wiki" button in a submenu that links to the edit-wiki page.
2569
*/
2570
static void wiki_submenu_to_edit_wiki(
2571
const char *zPrefix, /* "branch", "tag", or "checkin" */
2572
const char *zName, /* Name of the object */
2573
unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
2574
){
2575
if( g.perm.WrWiki && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
2576
style_submenu_element("Edit Wiki", "%R/wikiedit?name=%s/%t", zPrefix, zName);
2577
}
2578
}
2579
2580
/*
2581
** Check to see if there exists a wiki page with a name zPrefix/zName.
2582
** If there is, then render a <div class='section'>..</div> and
2583
** return true.
2584
**
2585
** If there is no such wiki page, return false.
2586
*/
2587
int wiki_render_associated(
2588
const char *zPrefix, /* "branch", "tag", "ticket", or "checkin" */
2589
const char *zName, /* Name of the object */
2590
unsigned int mFlags /* Zero or more WIKIASSOC_* flags */
2591
){
2592
int rid;
2593
Manifest *pWiki;
2594
if( !db_get_boolean("wiki-about",1) ) return 0;
2595
rid = db_int(0,
2596
"SELECT rid FROM tagxref"
2597
" WHERE tagid=(SELECT tagid FROM tag WHERE tagname='wiki-%q/%q')"
2598
" ORDER BY mtime DESC LIMIT 1",
2599
zPrefix, zName
2600
);
2601
pWiki = rid==0 ? 0 : manifest_get(rid, CFTYPE_WIKI, 0);
2602
if( pWiki==0 || pWiki->zWiki==0 || pWiki->zWiki[0]==0 ){
2603
if( g.perm.WrWiki && g.perm.Write && (mFlags & WIKIASSOC_MENU_WRITE)!=0 ){
2604
style_submenu_element("Add Wiki", "%R/wikiedit?name=%s/%t",
2605
zPrefix, zName);
2606
}
2607
return 0;
2608
}
2609
if( fossil_strcmp(pWiki->zMimetype, "text/x-markdown")==0 ){
2610
Blob tail = BLOB_INITIALIZER;
2611
Blob title = BLOB_INITIALIZER;
2612
Blob markdown;
2613
blob_init(&markdown, pWiki->zWiki, -1);
2614
markdown_to_html(&markdown, &title, &tail);
2615
if( blob_size(&title) ){
2616
@ <div class="section accordion">%h(blob_str(&title))</div>
2617
}else{
2618
wiki_section_label(zPrefix, zName, mFlags);
2619
}
2620
wiki_submenu_to_read_wiki(zPrefix, zName, mFlags);
2621
wiki_submenu_to_edit_wiki(zPrefix, zName, mFlags);
2622
@ <div class="accordion_panel">
2623
safe_html_context(DOCSRC_WIKI);
2624
safe_html(&tail);
2625
convert_href_and_output(&tail);
2626
@ </div>
2627
blob_reset(&tail);
2628
blob_reset(&title);
2629
blob_reset(&markdown);
2630
}else if( fossil_strcmp(pWiki->zMimetype, "text/plain")==0 ){
2631
wiki_section_label(zPrefix, zName, mFlags);
2632
wiki_submenu_to_edit_wiki(zPrefix, zName, mFlags);
2633
@ <div class="accordion_panel"><pre>
2634
@ %h(pWiki->zWiki)
2635
@ </pre></div>
2636
}else{
2637
Blob tail = BLOB_INITIALIZER;
2638
Blob title = BLOB_INITIALIZER;
2639
Blob wiki;
2640
Blob *pBody;
2641
blob_init(&wiki, pWiki->zWiki, -1);
2642
if( wiki_find_title(&wiki, &title, &tail) ){
2643
@ <div class="section accordion">%h(blob_str(&title))</div>
2644
pBody = &tail;
2645
}else{
2646
wiki_section_label(zPrefix, zName, mFlags);
2647
pBody = &wiki;
2648
}
2649
wiki_submenu_to_edit_wiki(zPrefix, zName, mFlags);
2650
@ <div class="accordion_panel"><div class="wiki">
2651
wiki_convert(pBody, 0, WIKI_BUTTONS);
2652
@ </div></div>
2653
blob_reset(&tail);
2654
blob_reset(&title);
2655
blob_reset(&wiki);
2656
}
2657
manifest_destroy(pWiki);
2658
builtin_request_js("accordion.js");
2659
return 1;
2660
}
2661

Keyboard Shortcuts

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