Fossil SCM

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

Keyboard Shortcuts

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