|
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 & 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 & 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">⚓</span></th> |
|
1857
|
@ <th>User<span hidden class="wh-clickable" |
|
1858
|
@ id="wh-collapser"> ♲</span></th> |
|
1859
|
if( showRid ){ |
|
1860
|
@ <th>RID</th> |
|
1861
|
} |
|
1862
|
@ <th> </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
|
|