Fossil SCM

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

Keyboard Shortcuts

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