Fossil

Artifact Content
Login

Artifact 1d2b4f904c5297379c8b4b0387982f7177a04cb14cec2377183eebb6de9be2fc:


     1  /*
     2  ** Copyright (c) 2007 D. Richard Hipp
     3  ** Copyright (c) 2008 Stephan Beal
     4  **
     5  ** This program is free software; you can redistribute it and/or
     6  ** modify it under the terms of the Simplified BSD License (also
     7  ** known as the "2-Clause License" or "FreeBSD License".)
     8  **
     9  ** This program is distributed in the hope that it will be useful,
    10  ** but without any warranty; without even the implied warranty of
    11  ** merchantability or fitness for a particular purpose.
    12  **
    13  ** Author contact information:
    14  **   drh@hwaci.com
    15  **   http://www.hwaci.com/drh/
    16  **
    17  *******************************************************************************
    18  **
    19  ** This file contains code to do formatting of wiki text.
    20  */
    21  #include "config.h"
    22  #include <assert.h>
    23  #include <ctype.h>
    24  #include "wiki.h"
    25  
    26  /*
    27  ** Return true if the input string is a well-formed wiki page name.
    28  **
    29  ** Well-formed wiki page names do not begin or end with whitespace,
    30  ** and do not contain tabs or other control characters and do not
    31  ** contain more than a single space character in a row.  Well-formed
    32  ** names must be between 1 and 100 characters in length, inclusive.
    33  */
    34  int wiki_name_is_wellformed(const unsigned char *z){
    35    int i;
    36    if( z[0]<=0x20 ){
    37      return 0;
    38    }
    39    for(i=1; z[i]; i++){
    40      if( z[i]<0x20 ) return 0;
    41      if( z[i]==0x20 && z[i-1]==0x20 ) return 0;
    42    }
    43    if( z[i-1]==' ' ) return 0;
    44    if( i<1 || i>100 ) return 0;
    45    return 1;
    46  }
    47  
    48  /*
    49  ** Output rules for well-formed wiki pages
    50  */
    51  static void well_formed_wiki_name_rules(void){
    52    @ <ul>
    53    @ <li> Must not begin or end with a space.</li>
    54    @ <li> Must not contain any control characters, including tab or
    55    @      newline.</li>
    56    @ <li> Must not have two or more spaces in a row internally.</li>
    57    @ <li> Must be between 1 and 100 characters in length.</li>
    58    @ </ul>
    59  }
    60  
    61  /*
    62  ** Check a wiki name.  If it is not well-formed, then issue an error
    63  ** and return true.  If it is well-formed, return false.
    64  */
    65  static int check_name(const char *z){
    66    if( !wiki_name_is_wellformed((const unsigned char *)z) ){
    67      style_header("Wiki Page Name Error");
    68      @ The wiki name "<span class="wikiError">%h(z)</span>" is not well-formed.
    69      @ Rules for wiki page names:
    70      well_formed_wiki_name_rules();
    71      style_footer();
    72      return 1;
    73    }
    74    return 0;
    75  }
    76  
    77  /*
    78  ** WEBPAGE: home
    79  ** WEBPAGE: index
    80  ** WEBPAGE: not_found
    81  **
    82  ** The /home, /index, and /not_found pages all redirect to the homepage
    83  ** configured by the administrator.
    84  */
    85  void home_page(void){
    86    char *zPageName = db_get("project-name",0);
    87    char *zIndexPage = db_get("index-page",0);
    88    login_check_credentials();
    89    if( zIndexPage ){
    90      const char *zPathInfo = P("PATH_INFO");
    91      while( zIndexPage[0]=='/' ) zIndexPage++;
    92      while( zPathInfo[0]=='/' ) zPathInfo++;
    93      if( fossil_strcmp(zIndexPage, zPathInfo)==0 ) zIndexPage = 0;
    94    }
    95    if( zIndexPage ){
    96      cgi_redirectf("%s/%s", g.zTop, zIndexPage);
    97    }
    98    if( !g.perm.RdWiki ){
    99      cgi_redirectf("%s/login?g=%s/home", g.zTop, g.zTop);
   100    }
   101    if( zPageName ){
   102      login_check_credentials();
   103      g.zExtra = zPageName;
   104      cgi_set_parameter_nocopy("name", g.zExtra, 1);
   105      g.isHome = 1;
   106      wiki_page();
   107      return;
   108    }
   109    style_header("Home");
   110    @ <p>This is a stub home-page for the project.
   111    @ To fill in this page, first go to
   112    @ %z(href("%R/setup_config"))setup/config</a>
   113    @ and establish a "Project Name".  Then create a
   114    @ wiki page with that name.  The content of that wiki page
   115    @ will be displayed in place of this message.</p>
   116    style_footer();
   117  }
   118  
   119  /*
   120  ** Return true if the given pagename is the name of the sandbox
   121  */
   122  static int is_sandbox(const char *zPagename){
   123    return fossil_stricmp(zPagename,"sandbox")==0 ||
   124           fossil_stricmp(zPagename,"sand box")==0;
   125  }
   126  
   127  /*
   128  ** Formal, common and short names for the various wiki styles.
   129  */
130 static const char *const azStyles[] = { 131 "text/x-fossil-wiki", "Fossil Wiki", "wiki", 132 "text/x-markdown", "Markdown", "markdown", 133 "text/plain", "Plain Text", "plain" 134 };
135 136 /* 137 ** Only allow certain mimetypes through. 138 ** All others become "text/x-fossil-wiki" 139 */ 140 const char *wiki_filter_mimetypes(const char *zMimetype){ 141 if( zMimetype!=0 ){ 142 int i; 143 for(i=0; i<count(azStyles); i+=3){ 144 if( fossil_strcmp(zMimetype,azStyles[i+2])==0 ){ 145 return azStyles[i]; 146 } 147 } 148 if( fossil_strcmp(zMimetype, "text/x-markdown")==0 149 || fossil_strcmp(zMimetype, "text/plain")==0 ){ 150 return zMimetype; 151 } 152 } 153 return "text/x-fossil-wiki"; 154 } 155 156 /* 157 ** Render wiki text according to its mimetype. 158 ** 159 ** text/x-fossil-wiki Fossil wiki 160 ** text/x-markdown Markdown 161 ** anything else... Plain text 162 */ 163 void wiki_render_by_mimetype(Blob *pWiki, const char *zMimetype){ 164 if( zMimetype==0 || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ 165 wiki_convert(pWiki, 0, 0); 166 }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ 167 Blob tail = BLOB_INITIALIZER; 168 markdown_to_html(pWiki, 0, &tail); 169 @ %s(blob_str(&tail)) 170 blob_reset(&tail); 171 }else{ 172 @ <pre class='textPlain'> 173 @ %h(blob_str(pWiki)) 174 @ </pre> 175 } 176 } 177 178 /* 179 ** WEBPAGE: md_rules 180 ** 181 ** Show a summary of the Markdown wiki formatting rules. 182 */ 183 void markdown_rules_page(void){ 184 Blob x; 185 int fTxt = P("txt")!=0; 186 style_header("Markdown Formatting Rules"); 187 if( fTxt ){ 188 style_submenu_element("Formatted", "%R/md_rules"); 189 }else{ 190 style_submenu_element("Plain-Text", "%R/md_rules?txt=1"); 191 } 192 blob_init(&x, builtin_text("markdown.md"), -1); 193 wiki_render_by_mimetype(&x, fTxt ? "text/plain" : "text/x-markdown"); 194 blob_reset(&x); 195 style_footer(); 196 } 197 198 /* 199 ** WEBPAGE: wiki_rules 200 ** 201 ** Show a summary of the wiki formatting rules. 202 */ 203 void wiki_rules_page(void){ 204 Blob x; 205 int fTxt = P("txt")!=0; 206 style_header("Wiki Formatting Rules"); 207 if( fTxt ){ 208 style_submenu_element("Formatted", "%R/wiki_rules"); 209 }else{ 210 style_submenu_element("Plain-Text", "%R/wiki_rules?txt=1"); 211 } 212 blob_init(&x, builtin_text("wiki.wiki"), -1); 213 wiki_render_by_mimetype(&x, fTxt ? "text/plain" : "text/x-fossil-wiki"); 214 blob_reset(&x); 215 style_footer(); 216 } 217 218 /* 219 ** Returns non-zero if moderation is required for wiki changes and wiki 220 ** attachments. 221 */ 222 int wiki_need_moderation( 223 int localUser /* Are we being called for a local interactive user? */ 224 ){ 225 /* 226 ** If the FOSSIL_FORCE_WIKI_MODERATION variable is set, *ALL* changes for 227 ** wiki pages will be required to go through moderation (even those performed 228 ** by the local interactive user via the command line). This can be useful 229 ** for local (or remote) testing of the moderation subsystem and its impact 230 ** on the contents and status of wiki pages. 231 */ 232 if( fossil_getenv("FOSSIL_FORCE_WIKI_MODERATION")!=0 ){ 233 return 1; 234 } 235 if( localUser ){ 236 return 0; 237 } 238 return g.perm.ModWiki==0 && db_get_boolean("modreq-wiki",0)==1; 239 } 240 241 /* Standard submenu items for wiki pages */ 242 #define W_SRCH 0x00001 243 #define W_LIST 0x00002 244 #define W_HELP 0x00004 245 #define W_NEW 0x00008 246 #define W_BLOG 0x00010 247 #define W_SANDBOX 0x00020 248 #define W_ALL 0x0001f 249 #define W_ALL_BUT(x) (W_ALL&~(x)) 250 251 /* 252 ** Add some standard submenu elements for wiki screens. 253 */ 254 static void wiki_standard_submenu(unsigned int ok){ 255 if( (ok & W_SRCH)!=0 && search_restrict(SRCH_WIKI)!=0 ){ 256 style_submenu_element("Search", "%R/wikisrch"); 257 } 258 if( (ok & W_LIST)!=0 ){ 259 style_submenu_element("List", "%R/wcontent"); 260 } 261 if( (ok & W_HELP)!=0 ){ 262 style_submenu_element("Help", "%R/wikihelp"); 263 } 264 if( (ok & W_NEW)!=0 && g.anon.NewWiki ){ 265 style_submenu_element("New", "%R/wikinew"); 266 } 267 #if 0 268 if( (ok & W_BLOG)!=0 269 #endif 270 if( (ok & W_SANDBOX)!=0 ){ 271 style_submenu_element("Sandbox", "%R/wiki?name=Sandbox"); 272 } 273 } 274 275 /* 276 ** WEBPAGE: wikihelp 277 ** A generic landing page for wiki. 278 */ 279 void wiki_helppage(void){ 280 login_check_credentials(); 281 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } 282 style_header("Wiki Help"); 283 wiki_standard_submenu(W_ALL_BUT(W_HELP)); 284 @ <h2>Wiki Links</h2> 285 @ <ul> 286 { char *zWikiHomePageName = db_get("index-page",0); 287 if( zWikiHomePageName ){ 288 @ <li> %z(href("%R%s",zWikiHomePageName)) 289 @ %h(zWikiHomePageName)</a> wiki home page.</li> 290 } 291 } 292 { char *zHomePageName = db_get("project-name",0); 293 if( zHomePageName ){ 294 @ <li> %z(href("%R/wiki?name=%t",zHomePageName)) 295 @ %h(zHomePageName)</a> project home page.</li> 296 } 297 } 298 @ <li> %z(href("%R/timeline?y=w"))Recent changes</a> to wiki pages.</li> 299 @ <li> Formatting rules for %z(href("%R/wiki_rules"))Fossil Wiki</a> and for 300 @ %z(href("%R/md_rules"))Markdown Wiki</a>.</li> 301 @ <li> Use the %z(href("%R/wiki?name=Sandbox"))Sandbox</a> 302 @ to experiment.</li> 303 if( g.anon.NewWiki ){ 304 @ <li> Create a %z(href("%R/wikinew"))new wiki page</a>.</li> 305 if( g.anon.Write ){ 306 @ <li> Create a %z(href("%R/technoteedit"))new tech-note</a>.</li> 307 } 308 } 309 @ <li> %z(href("%R/wcontent"))List of All Wiki Pages</a> 310 @ available on this server.</li> 311 if( g.anon.ModWiki ){ 312 @ <li> %z(href("%R/modreq"))Tend to pending moderation requests</a></li> 313 } 314 if( search_restrict(SRCH_WIKI)!=0 ){ 315 @ <li> %z(href("%R/wikisrch"))Search</a> for wiki pages containing key 316 @ words</li> 317 } 318 @ </ul> 319 style_footer(); 320 return; 321 } 322 323 /* 324 ** WEBPAGE: wikisrch 325 ** Usage: /wikisrch?s=PATTERN 326 ** 327 ** Full-text search of all current wiki text 328 */ 329 void wiki_srchpage(void){ 330 login_check_credentials(); 331 style_header("Wiki Search"); 332 wiki_standard_submenu(W_HELP|W_LIST|W_SANDBOX); 333 search_screen(SRCH_WIKI, 0); 334 style_footer(); 335 } 336 337 /* 338 ** WEBPAGE: wiki 339 ** URL: /wiki?name=PAGENAME 340 */ 341 void wiki_page(void){ 342 char *zTag; 343 int rid = 0; 344 int isSandbox; 345 char *zUuid; 346 unsigned submenuFlags = W_ALL; 347 Blob wiki; 348 Manifest *pWiki = 0; 349 const char *zPageName; 350 const char *zMimetype = 0; 351 char *zBody = mprintf("%s","<i>Empty Page</i>"); 352 353 login_check_credentials(); 354 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } 355 zPageName = P("name"); 356 if( zPageName==0 ){ 357 if( search_restrict(SRCH_WIKI)!=0 ){ 358 wiki_srchpage(); 359 }else{ 360 wiki_helppage(); 361 } 362 return; 363 } 364 if( check_name(zPageName) ) return; 365 isSandbox = is_sandbox(zPageName); 366 if( isSandbox ){ 367 submenuFlags &= ~W_SANDBOX; 368 zBody = db_get("sandbox",zBody); 369 zMimetype = db_get("sandbox-mimetype","text/x-fossil-wiki"); 370 rid = 0; 371 }else{ 372 const char *zUuid = P("id"); 373 if( zUuid==0 || (rid = symbolic_name_to_rid(zUuid,"w"))==0 ){ 374 zTag = mprintf("wiki-%s", zPageName); 375 rid = db_int(0, 376 "SELECT rid FROM tagxref" 377 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" 378 " ORDER BY mtime DESC", zTag 379 ); 380 free(zTag); 381 } 382 pWiki = manifest_get(rid, CFTYPE_WIKI, 0); 383 if( pWiki ){ 384 zBody = pWiki->zWiki; 385 zMimetype = pWiki->zMimetype; 386 } 387 } 388 zMimetype = wiki_filter_mimetypes(zMimetype); 389 if( !g.isHome ){ 390 if( rid ){ 391 style_submenu_element("Diff", "%R/wdiff?name=%T&a=%d", zPageName, rid); 392 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); 393 style_submenu_element("Details", "%R/info/%s", zUuid); 394 } 395 if( (rid && g.anon.WrWiki) || (!rid && g.anon.NewWiki) ){ 396 if( db_get_boolean("wysiwyg-wiki", 0) ){ 397 style_submenu_element("Edit", "%s/wikiedit?name=%T&wysiwyg=1", 398 g.zTop, zPageName); 399 }else{ 400 style_submenu_element("Edit", "%s/wikiedit?name=%T", g.zTop, zPageName); 401 } 402 } 403 if( rid && g.anon.ApndWiki && g.anon.Attach ){ 404 style_submenu_element("Attach", 405 "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T", 406 g.zTop, zPageName, g.zTop, zPageName); 407 } 408 if( rid && g.anon.ApndWiki ){ 409 style_submenu_element("Append", "%s/wikiappend?name=%T&mimetype=%s", 410 g.zTop, zPageName, zMimetype); 411 } 412 if( g.perm.Hyperlink ){ 413 style_submenu_element("History", "%s/whistory?name=%T", 414 g.zTop, zPageName); 415 } 416 } 417 style_set_current_page("%T?name=%T", g.zPath, zPageName); 418 style_header("%s", zPageName); 419 wiki_standard_submenu(submenuFlags); 420 blob_init(&wiki, zBody, -1); 421 wiki_render_by_mimetype(&wiki, zMimetype); 422 blob_reset(&wiki); 423 attachment_list(zPageName, "<hr /><h2>Attachments:</h2><ul>"); 424 manifest_destroy(pWiki); 425 style_footer(); 426 } 427 428 /* 429 ** Write a wiki artifact into the repository 430 */ 431 int wiki_put(Blob *pWiki, int parent, int needMod){ 432 int nrid; 433 if( !needMod ){ 434 nrid = content_put_ex(pWiki, 0, 0, 0, 0); 435 if( parent) content_deltify(parent, &nrid, 1, 0); 436 }else{ 437 nrid = content_put_ex(pWiki, 0, 0, 0, 1); 438 moderation_table_create(); 439 db_multi_exec("INSERT INTO modreq(objid) VALUES(%d)", nrid); 440 } 441 db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", nrid); 442 db_multi_exec("INSERT OR IGNORE INTO unclustered VALUES(%d);", nrid); 443 manifest_crosslink(nrid, pWiki, MC_NONE); 444 return nrid; 445 } 446 447 /* 448 ** Output a selection box from which the user can select the 449 ** wiki mimetype. 450 */ 451 void mimetype_option_menu(const char *zMimetype){ 452 unsigned i; 453 @ <select name="mimetype" size="1"> 454 for(i=0; i<count(azStyles); i+=3){ 455 if( fossil_strcmp(zMimetype,azStyles[i])==0 ){ 456 @ <option value="%s(azStyles[i])" selected>%s(azStyles[i+1])</option> 457 }else{ 458 @ <option value="%s(azStyles[i])">%s(azStyles[i+1])</option> 459 } 460 } 461 @ </select> 462 } 463 464 /* 465 ** Given a mimetype, return its common name. 466 */ 467 static const char *mimetype_common_name(const char *zMimetype){ 468 int i; 469 for(i=4; i>=2; i-=2){ 470 if( zMimetype && fossil_strcmp(zMimetype, azStyles[i])==0 ){ 471 return azStyles[i+1]; 472 } 473 } 474 return azStyles[1]; 475 } 476 477 /* 478 ** WEBPAGE: wikiedit 479 ** URL: /wikiedit?name=PAGENAME 480 ** 481 ** Edit a wiki page. 482 */ 483 void wikiedit_page(void){ 484 char *zTag; 485 int rid = 0; 486 int isSandbox; 487 Blob wiki; 488 Manifest *pWiki = 0; 489 const char *zPageName; 490 int n; 491 const char *z; 492 char *zBody = (char*)P("w"); 493 const char *zMimetype = wiki_filter_mimetypes(P("mimetype")); 494 int isWysiwyg = P("wysiwyg")!=0; 495 int goodCaptcha = 1; 496 497 if( P("edit-wysiwyg")!=0 ){ isWysiwyg = 1; zBody = 0; } 498 if( P("edit-markup")!=0 ){ isWysiwyg = 0; zBody = 0; } 499 if( zBody ){ 500 if( isWysiwyg ){ 501 Blob body; 502 blob_zero(&body); 503 htmlTidy(zBody, &body); 504 zBody = blob_str(&body); 505 }else{ 506 zBody = mprintf("%s", zBody); 507 } 508 } 509 login_check_credentials(); 510 zPageName = PD("name",""); 511 if( check_name(zPageName) ) return; 512 isSandbox = is_sandbox(zPageName); 513 if( isSandbox ){ 514 if( !g.perm.WrWiki ){ 515 login_needed(g.anon.WrWiki); 516 return; 517 } 518 if( zBody==0 ){ 519 zBody = db_get("sandbox",""); 520 zMimetype = db_get("sandbox-mimetype","text/x-fossil-wiki"); 521 } 522 }else{ 523 zTag = mprintf("wiki-%s", zPageName); 524 rid = db_int(0, 525 "SELECT rid FROM tagxref" 526 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" 527 " ORDER BY mtime DESC", zTag 528 ); 529 free(zTag); 530 if( (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ 531 login_needed(rid ? g.anon.WrWiki : g.anon.NewWiki); 532 return; 533 } 534 if( zBody==0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ 535 zBody = pWiki->zWiki; 536 zMimetype = pWiki->zMimetype; 537 } 538 } 539 if( P("submit")!=0 && zBody!=0 540 && (goodCaptcha = captcha_is_correct(0)) 541 ){ 542 char *zDate; 543 Blob cksum; 544 blob_zero(&wiki); 545 db_begin_transaction(); 546 if( isSandbox ){ 547 db_set("sandbox",zBody,0); 548 db_set("sandbox-mimetype",zMimetype,0); 549 }else{ 550 login_verify_csrf_secret(); 551 zDate = date_in_standard_format("now"); 552 blob_appendf(&wiki, "D %s\n", zDate); 553 free(zDate); 554 blob_appendf(&wiki, "L %F\n", zPageName); 555 if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")!=0 ){ 556 blob_appendf(&wiki, "N %s\n", zMimetype); 557 } 558 if( rid ){ 559 char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); 560 blob_appendf(&wiki, "P %s\n", zUuid); 561 free(zUuid); 562 } 563 if( !login_is_nobody() ){ 564 blob_appendf(&wiki, "U %F\n", login_name()); 565 } 566 blob_appendf(&wiki, "W %d\n%s\n", strlen(zBody), zBody); 567 md5sum_blob(&wiki, &cksum); 568 blob_appendf(&wiki, "Z %b\n", &cksum); 569 blob_reset(&cksum); 570 wiki_put(&wiki, 0, wiki_need_moderation(0)); 571 } 572 db_end_transaction(0); 573 cgi_redirectf("wiki?name=%T", zPageName); 574 } 575 if( P("cancel")!=0 ){ 576 cgi_redirectf("wiki?name=%T", zPageName); 577 return; 578 } 579 if( zBody==0 ){ 580 zBody = mprintf("<i>Empty Page</i>"); 581 } 582 style_set_current_page("%T?name=%T", g.zPath, zPageName); 583 style_header("Edit: %s", zPageName); 584 if( !goodCaptcha ){ 585 @ <p class="generalError">Error: Incorrect security code.</p> 586 } 587 blob_zero(&wiki); 588 blob_append(&wiki, zBody, -1); 589 if( P("preview")!=0 ){ 590 @ Preview:<hr /> 591 wiki_render_by_mimetype(&wiki, zMimetype); 592 @ <hr /> 593 blob_reset(&wiki); 594 } 595 for(n=2, z=zBody; z[0]; z++){ 596 if( z[0]=='\n' ) n++; 597 } 598 if( n<20 ) n = 20; 599 if( n>30 ) n = 30; 600 if( !isWysiwyg ){ 601 /* Traditional markup-only editing */ 602 form_begin(0, "%R/wikiedit"); 603 @ <div>Markup style: 604 mimetype_option_menu(zMimetype); 605 @ <br /><textarea name="w" class="wikiedit" cols="80" 606 @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea> 607 @ <br /> 608 if( db_get_boolean("wysiwyg-wiki", 0) ){ 609 @ <input type="submit" name="edit-wysiwyg" value="Wysiwyg Editor" 610 @ onclick='return confirm("Switching to WYSIWYG-mode\nwill erase your markup\nedits. Continue?")' /> 611 } 612 @ <input type="submit" name="preview" value="Preview Your Changes" /> 613 }else{ 614 /* Wysiwyg editing */ 615 Blob html, temp; 616 form_begin("onsubmit='wysiwygSubmit()'", "%R/wikiedit"); 617 @ <div> 618 @ <input type="hidden" name="wysiwyg" value="1" /> 619 blob_zero(&temp); 620 wiki_convert(&wiki, &temp, 0); 621 blob_zero(&html); 622 htmlTidy(blob_str(&temp), &html); 623 blob_reset(&temp); 624 wysiwygEditor("w", blob_str(&html), 60, n); 625 blob_reset(&html); 626 @ <br /> 627 @ <input type="submit" name="edit-markup" value="Markup Editor" 628 @ onclick='return confirm("Switching to markup-mode\nwill erase your WYSIWYG\nedits. Continue?")' /> 629 } 630 login_insert_csrf_secret(); 631 @ <input type="submit" name="submit" value="Apply These Changes" /> 632 @ <input type="hidden" name="name" value="%h(zPageName)" /> 633 @ <input type="submit" name="cancel" value="Cancel" 634 @ onclick='confirm("Abandon your changes?")' /> 635 @ </div> 636 captcha_generate(0); 637 @ </form> 638 manifest_destroy(pWiki); 639 blob_reset(&wiki); 640 style_footer(); 641 } 642 643 /* 644 ** WEBPAGE: wikinew 645 ** URL /wikinew 646 ** 647 ** Prompt the user to enter the name of a new wiki page. Then redirect 648 ** to the wikiedit screen for that new page. 649 */ 650 void wikinew_page(void){ 651 const char *zName; 652 const char *zMimetype; 653 login_check_credentials(); 654 if( !g.perm.NewWiki ){ 655 login_needed(g.anon.NewWiki); 656 return; 657 } 658 zName = PD("name",""); 659 zMimetype = wiki_filter_mimetypes(P("mimetype")); 660 if( zName[0] && wiki_name_is_wellformed((const unsigned char *)zName) ){ 661 if( fossil_strcmp(zMimetype,"text/x-fossil-wiki")==0 662 && db_get_boolean("wysiwyg-wiki", 0) 663 ){ 664 cgi_redirectf("wikiedit?name=%T&wysiwyg=1", zName); 665 }else{ 666 cgi_redirectf("wikiedit?name=%T&mimetype=%s", zName, zMimetype); 667 } 668 } 669 style_header("Create A New Wiki Page"); 670 wiki_standard_submenu(W_ALL_BUT(W_NEW)); 671 @ <p>Rules for wiki page names:</p> 672 well_formed_wiki_name_rules(); 673 form_begin(0, "%R/wikinew"); 674 @ <p>Name of new wiki page: 675 @ <input style="width: 35;" type="text" name="name" value="%h(zName)" /><br /> 676 @ Markup style: 677 mimetype_option_menu("text/x-fossil-wiki"); 678 @ <br /><input type="submit" value="Create" /> 679 @ </p></form> 680 if( zName[0] ){ 681 @ <p><span class="wikiError"> 682 @ "%h(zName)" is not a valid wiki page name!</span></p> 683 } 684 style_footer(); 685 } 686 687 688 /* 689 ** Append the wiki text for an remark to the end of the given BLOB. 690 */ 691 static void appendRemark(Blob *p, const char *zMimetype){ 692 char *zDate; 693 const char *zUser; 694 const char *zRemark; 695 char *zId; 696 697 zDate = db_text(0, "SELECT datetime('now')"); 698 zRemark = PD("r",""); 699 zUser = PD("u",g.zLogin); 700 if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ 701 zId = db_text(0, "SELECT lower(hex(randomblob(8)))"); 702 blob_appendf(p, "\n\n<hr /><div id=\"%s\"><i>On %s UTC %h", 703 zId, zDate, login_name()); 704 if( zUser[0] && fossil_strcmp(zUser,login_name()) ){ 705 blob_appendf(p, " (claiming to be %h)", zUser); 706 } 707 blob_appendf(p, " added:</i><br />\n%s</div id=\"%s\">", zRemark, zId); 708 }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ 709 blob_appendf(p, "\n\n------\n*On %s UTC %h", zDate, login_name()); 710 if( zUser[0] && fossil_strcmp(zUser,login_name()) ){ 711 blob_appendf(p, " (claiming to be %h)", zUser); 712 } 713 blob_appendf(p, " added:*\n\n%s\n", zRemark); 714 }else{ 715 blob_appendf(p, "\n\n------------------------------------------------\n" 716 "On %s UTC %s", zDate, login_name()); 717 if( zUser[0] && fossil_strcmp(zUser,login_name()) ){ 718 blob_appendf(p, " (claiming to be %s)", zUser); 719 } 720 blob_appendf(p, " added:\n\n%s\n", zRemark); 721 } 722 fossil_free(zDate); 723 } 724 725 /* 726 ** WEBPAGE: wikiappend 727 ** URL: /wikiappend?name=PAGENAME&mimetype=MIMETYPE 728 ** 729 ** Append text to the end of a wiki page. 730 */ 731 void wikiappend_page(void){ 732 char *zTag; 733 int rid = 0; 734 int isSandbox; 735 const char *zPageName; 736 const char *zUser; 737 const char *zMimetype; 738 int goodCaptcha = 1; 739 const char *zFormat; 740 741 login_check_credentials(); 742 zPageName = PD("name",""); 743 zMimetype = wiki_filter_mimetypes(P("mimetype")); 744 if( check_name(zPageName) ) return; 745 isSandbox = is_sandbox(zPageName); 746 if( !isSandbox ){ 747 zTag = mprintf("wiki-%s", zPageName); 748 rid = db_int(0, 749 "SELECT rid FROM tagxref" 750 " WHERE tagid=(SELECT tagid FROM tag WHERE tagname=%Q)" 751 " ORDER BY mtime DESC", zTag 752 ); 753 free(zTag); 754 if( !rid ){ 755 fossil_redirect_home(); 756 return; 757 } 758 } 759 if( !g.perm.ApndWiki ){ 760 login_needed(g.anon.ApndWiki); 761 return; 762 } 763 if( P("submit")!=0 && P("r")!=0 && P("u")!=0 764 && (goodCaptcha = captcha_is_correct(0)) 765 ){ 766 char *zDate; 767 Blob cksum; 768 Blob body; 769 Blob wiki; 770 Manifest *pWiki = 0; 771 772 blob_zero(&body); 773 if( isSandbox ){ 774 blob_append(&body, db_get("sandbox",""), -1); 775 appendRemark(&body, zMimetype); 776 db_set("sandbox", blob_str(&body), 0); 777 }else{ 778 login_verify_csrf_secret(); 779 pWiki = manifest_get(rid, CFTYPE_WIKI, 0); 780 if( pWiki ){ 781 blob_append(&body, pWiki->zWiki, -1); 782 manifest_destroy(pWiki); 783 } 784 blob_zero(&wiki); 785 db_begin_transaction(); 786 zDate = date_in_standard_format("now"); 787 blob_appendf(&wiki, "D %s\n", zDate); 788 blob_appendf(&wiki, "L %F\n", zPageName); 789 if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")!=0 ){ 790 blob_appendf(&wiki, "N %s\n", zMimetype); 791 } 792 if( rid ){ 793 char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); 794 blob_appendf(&wiki, "P %s\n", zUuid); 795 free(zUuid); 796 } 797 if( !login_is_nobody() ){ 798 blob_appendf(&wiki, "U %F\n", login_name()); 799 } 800 appendRemark(&body, zMimetype); 801 blob_appendf(&wiki, "W %d\n%s\n", blob_size(&body), blob_str(&body)); 802 md5sum_blob(&wiki, &cksum); 803 blob_appendf(&wiki, "Z %b\n", &cksum); 804 blob_reset(&cksum); 805 wiki_put(&wiki, rid, wiki_need_moderation(0)); 806 db_end_transaction(0); 807 } 808 cgi_redirectf("wiki?name=%T", zPageName); 809 } 810 if( P("cancel")!=0 ){ 811 cgi_redirectf("wiki?name=%T", zPageName); 812 return; 813 } 814 style_set_current_page("%T?name=%T", g.zPath, zPageName); 815 style_header("Append Comment To: %s", zPageName); 816 if( !goodCaptcha ){ 817 @ <p class="generalError">Error: Incorrect security code.</p> 818 } 819 if( P("preview")!=0 ){ 820 Blob preview; 821 blob_zero(&preview); 822 appendRemark(&preview, zMimetype); 823 @ Preview:<hr /> 824 wiki_render_by_mimetype(&preview, zMimetype); 825 @ <hr /> 826 blob_reset(&preview); 827 } 828 zUser = PD("u", g.zLogin); 829 form_begin(0, "%R/wikiappend"); 830 login_insert_csrf_secret(); 831 @ <input type="hidden" name="name" value="%h(zPageName)" /> 832 @ <input type="hidden" name="mimetype" value="%h(zMimetype)" /> 833 @ Your Name: 834 @ <input type="text" name="u" size="20" value="%h(zUser)" /><br /> 835 zFormat = mimetype_common_name(zMimetype); 836 @ Comment to append (formatted as %s(zFormat)):<br /> 837 @ <textarea name="r" class="wikiedit" cols="80" 838 @ rows="10" wrap="virtual">%h(PD("r",""))</textarea> 839 @ <br /> 840 @ <input type="submit" name="preview" value="Preview Your Comment" /> 841 @ <input type="submit" name="submit" value="Append Your Changes" /> 842 @ <input type="submit" name="cancel" value="Cancel" /> 843 captcha_generate(0); 844 @ </form> 845 style_footer(); 846 } 847 848 /* 849 ** Name of the wiki history page being generated 850 */ 851 static const char *zWikiPageName; 852 853 /* 854 ** Function called to output extra text at the end of each line in 855 ** a wiki history listing. 856 */ 857 static void wiki_history_extra(int rid){ 858 if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){ 859 @ %z(href("%R/wdiff?name=%t&a=%d",zWikiPageName,rid))[diff]</a> 860 } 861 } 862 863 /* 864 ** WEBPAGE: whistory 865 ** URL: /whistory?name=PAGENAME 866 ** 867 ** Show the complete change history for a single wiki page. 868 */ 869 void whistory_page(void){ 870 Stmt q; 871 const char *zPageName; 872 login_check_credentials(); 873 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; } 874 zPageName = PD("name",""); 875 style_header("History Of %s", zPageName); 876 877 db_prepare(&q, "%s AND event.objid IN " 878 " (SELECT rid FROM tagxref WHERE tagid=" 879 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')" 880 " UNION SELECT attachid FROM attachment" 881 " WHERE target=%Q)" 882 "ORDER BY mtime DESC", 883 timeline_query_for_www(), zPageName, zPageName); 884 zWikiPageName = zPageName; 885 www_print_timeline(&q, TIMELINE_ARTID, 0, 0, 0, wiki_history_extra); 886 db_finalize(&q); 887 style_footer(); 888 } 889 890 /* 891 ** WEBPAGE: wdiff 892 ** URL: /whistory?name=PAGENAME&a=RID1&b=RID2 893 ** 894 ** Show the difference between two wiki pages. 895 */ 896 void wdiff_page(void){ 897 int rid1, rid2; 898 const char *zPageName; 899 Manifest *pW1, *pW2 = 0; 900 Blob w1, w2, d; 901 u64 diffFlags; 902 903 login_check_credentials(); 904 rid1 = atoi(PD("a","0")); 905 if( !g.perm.Hyperlink ){ login_needed(g.anon.Hyperlink); return; } 906 if( rid1==0 ) fossil_redirect_home(); 907 rid2 = atoi(PD("b","0")); 908 zPageName = PD("name",""); 909 style_header("Changes To %s", zPageName); 910 911 if( rid2==0 ){ 912 rid2 = db_int(0, 913 "SELECT objid FROM event JOIN tagxref ON objid=rid AND tagxref.tagid=" 914 "(SELECT tagid FROM tag WHERE tagname='wiki-%q')" 915 " WHERE event.mtime<(SELECT mtime FROM event WHERE objid=%d)" 916 " ORDER BY event.mtime DESC LIMIT 1", 917 zPageName, rid1 918 ); 919 } 920 pW1 = manifest_get(rid1, CFTYPE_WIKI, 0); 921 if( pW1==0 ) fossil_redirect_home(); 922 blob_init(&w1, pW1->zWiki, -1); 923 blob_zero(&w2); 924 if( rid2 && (pW2 = manifest_get(rid2, CFTYPE_WIKI, 0))!=0 ){ 925 blob_init(&w2, pW2->zWiki, -1); 926 } 927 blob_zero(&d); 928 diffFlags = construct_diff_flags(1); 929 text_diff(&w2, &w1, &d, 0, diffFlags | DIFF_HTML | DIFF_LINENO); 930 @ <pre class="udiff"> 931 @ %s(blob_str(&d)) 932 @ <pre> 933 manifest_destroy(pW1); 934 manifest_destroy(pW2); 935 style_footer(); 936 } 937 938 /* 939 ** prepare()s pStmt with a query requesting: 940 ** 941 ** - wiki page name 942 ** - tagxref (whatever that really is!) 943 ** 944 ** Used by wcontent_page() and the JSON wiki code. 945 */ 946 void wiki_prepare_page_list( Stmt * pStmt ){ 947 db_prepare(pStmt, 948 "SELECT" 949 " substr(tagname, 6) as name," 950 " (SELECT value FROM tagxref WHERE tagid=tag.tagid" 951 " ORDER BY mtime DESC) as tagXref" 952 " FROM tag WHERE tagname GLOB 'wiki-*'" 953 " ORDER BY lower(tagname) /*sort*/" 954 ); 955 } 956 /* 957 ** WEBPAGE: wcontent 958 ** 959 ** all=1 Show deleted pages 960 ** 961 ** List all available wiki pages with date created and last modified. 962 */ 963 void wcontent_page(void){ 964 Stmt q; 965 int showAll = P("all")!=0; 966 967 login_check_credentials(); 968 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } 969 style_header("Available Wiki Pages"); 970 if( showAll ){ 971 style_submenu_element("Active", "%s/wcontent", g.zTop); 972 }else{ 973 style_submenu_element("All", "%s/wcontent?all=1", g.zTop); 974 } 975 wiki_standard_submenu(W_ALL_BUT(W_LIST)); 976 @ <ul> 977 wiki_prepare_page_list(&q); 978 while( db_step(&q)==SQLITE_ROW ){ 979 const char *zName = db_column_text(&q, 0); 980 int size = db_column_int(&q, 1); 981 if( size>0 ){ 982 @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li> 983 }else if( showAll ){ 984 @ <li>%z(href("%R/wiki?name=%T",zName))<s>%h(zName)</s></a></li> 985 } 986 } 987 db_finalize(&q); 988 @ </ul> 989 style_footer(); 990 } 991 992 /* 993 ** WEBPAGE: wfind 994 ** 995 ** URL: /wfind?title=TITLE 996 ** List all wiki pages whose titles contain the search text 997 */ 998 void wfind_page(void){ 999 Stmt q; 1000 const char *zTitle; 1001 login_check_credentials(); 1002 if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } 1003 zTitle = PD("title","*"); 1004 style_header("Wiki Pages Found"); 1005 @ <ul> 1006 db_prepare(&q, 1007 "SELECT substr(tagname, 6, 1000) FROM tag WHERE tagname like 'wiki-%%%q%%'" 1008 " ORDER BY lower(tagname) /*sort*/" , 1009 zTitle); 1010 while( db_step(&q)==SQLITE_ROW ){ 1011 const char *zName = db_column_text(&q, 0); 1012 @ <li>%z(href("%R/wiki?name=%T",zName))%h(zName)</a></li> 1013 } 1014 db_finalize(&q); 1015 @ </ul> 1016 style_footer(); 1017 } 1018 1019 /* 1020 ** Add a new wiki page to the repository. The page name is 1021 ** given by the zPageName parameter. rid must be zero to create 1022 ** a new page otherwise the page identified by rid is updated. 1023 ** 1024 ** The content of the new page is given by the blob pContent. 1025 ** 1026 ** zMimeType specifies the N-card for the wiki page. If it is 0, 1027 ** empty, or "text/x-fossil-wiki" (the default format) then it is 1028 ** ignored. 1029 */ 1030 int wiki_cmd_commit(const char *zPageName, int rid, Blob *pContent, 1031 const char *zMimeType, int localUser){ 1032 Blob wiki; /* Wiki page content */ 1033 Blob cksum; /* wiki checksum */ 1034 char *zDate; /* timestamp */ 1035 char *zUuid; /* uuid for rid */ 1036 1037 blob_zero(&wiki); 1038 zDate = date_in_standard_format("now"); 1039 blob_appendf(&wiki, "D %s\n", zDate); 1040 free(zDate); 1041 blob_appendf(&wiki, "L %F\n", zPageName ); 1042 if( zMimeType && *zMimeType 1043 && 0!=fossil_strcmp(zMimeType,"text/x-fossil-wiki") ){ 1044 blob_appendf(&wiki, "N %F\n", zMimeType); 1045 } 1046 if( rid ){ 1047 zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); 1048 blob_appendf(&wiki, "P %s\n", zUuid); 1049 free(zUuid); 1050 } 1051 user_select(); 1052 if( !login_is_nobody() ){ 1053 blob_appendf(&wiki, "U %F\n", login_name()); 1054 } 1055 blob_appendf( &wiki, "W %d\n%s\n", blob_size(pContent), 1056 blob_str(pContent) ); 1057 md5sum_blob(&wiki, &cksum); 1058 blob_appendf(&wiki, "Z %b\n", &cksum); 1059 blob_reset(&cksum); 1060 db_begin_transaction(); 1061 wiki_put(&wiki, 0, wiki_need_moderation(localUser)); 1062 db_end_transaction(0); 1063 return 1; 1064 } 1065 1066 /* 1067 ** Determine the rid for a tech note given either its id or its 1068 ** timestamp. Returns 0 if there is no such item and -1 if the details 1069 ** are ambiguous and could refer to multiple items. 1070 */ 1071 int wiki_technote_to_rid(const char *zETime) { 1072 int rid=0; /* Artifact ID of the tech note */ 1073 int nETime = strlen(zETime); 1074 Stmt q; 1075 if( nETime>=4 && nETime<=HNAME_MAX && validate16(zETime, nETime) ){ 1076 char zUuid[HNAME_MAX+1]; 1077 memcpy(zUuid, zETime, nETime+1); 1078 canonical16(zUuid, nETime); 1079 db_prepare(&q, 1080 "SELECT e.objid" 1081 " FROM event e, tag t" 1082 " WHERE e.type='e' AND e.tagid IS NOT NULL AND t.tagid=e.tagid" 1083 " AND t.tagname GLOB 'event-%q*'", 1084 zUuid 1085 ); 1086 if( db_step(&q)==SQLITE_ROW ){ 1087 rid = db_column_int(&q, 0); 1088 if( db_step(&q)==SQLITE_ROW ) rid = -1; 1089 } 1090 db_finalize(&q); 1091 } 1092 if (!rid) { 1093 if (strlen(zETime)>4) { 1094 rid = db_int(0, "SELECT objid" 1095 " FROM event" 1096 " WHERE datetime(mtime)=datetime('%q')" 1097 " AND type='e'" 1098 " AND tagid IS NOT NULL" 1099 " ORDER BY objid DESC LIMIT 1", 1100 zETime); 1101 } 1102 } 1103 return rid; 1104 } 1105 1106 /* 1107 ** COMMAND: wiki* 1108 ** 1109 ** Usage: %fossil wiki (export|create|commit|list) WikiName 1110 ** 1111 ** Run various subcommands to work with wiki entries or tech notes. 1112 ** 1113 ** %fossil wiki export PAGENAME ?FILE? 1114 ** %fossil wiki export ?FILE? -t|--technote DATETIME|TECHNOTE-ID 1115 ** 1116 ** Sends the latest version of either a wiki page or of a tech note 1117 ** to the given file or standard output. 1118 ** If PAGENAME is provided, the wiki page will be output. For 1119 ** a tech note either DATETIME or TECHNOTE-ID must be specified. If 1120 ** DATETIME is used, the most recently modified tech note with that 1121 ** DATETIME will be sent. 1122 ** 1123 ** %fossil wiki (create|commit) PAGENAME ?FILE? ?OPTIONS? 1124 ** 1125 ** Create a new or commit changes to an existing wiki page or 1126 ** technote from FILE or from standard input. PAGENAME is the 1127 ** name of the wiki entry or the timeline comment of the 1128 ** technote. 1129 ** 1130 ** Options: 1131 ** -M|--mimetype TEXT-FORMAT The mime type of the update. 1132 ** Defaults to the type used by 1133 ** the previous version of the 1134 ** page, or text/x-fossil-wiki. 1135 ** Valid values are: text/x-fossil-wiki, 1136 ** text/markdown and text/plain. fossil, 1137 ** markdown or plain can be specified as 1138 ** synonyms of these values. 1139 ** -t|--technote DATETIME Specifies the timestamp of 1140 ** the technote to be created or 1141 ** updated. When updating a tech note 1142 ** the most recently modified tech note 1143 ** with the specified timestamp will be 1144 ** updated. 1145 ** -t|--technote TECHNOTE-ID Specifies the technote to be 1146 ** updated by its technote id. 1147 ** --technote-tags TAGS The set of tags for a technote. 1148 ** --technote-bgcolor COLOR The color used for the technote 1149 ** on the timeline. 1150 ** 1151 ** %fossil wiki list ?OPTIONS? 1152 ** %fossil wiki ls ?OPTIONS? 1153 ** 1154 ** Lists all wiki entries, one per line, ordered 1155 ** case-insensitively by name. 1156 ** 1157 ** Options: 1158 ** -t|--technote Technotes will be listed instead of 1159 ** pages. The technotes will be in order 1160 ** of timestamp with the most recent 1161 ** first. 1162 ** -s|--show-technote-ids The id of the tech note will be listed 1163 ** along side the timestamp. The tech note 1164 ** id will be the first word on each line. 1165 ** This option only applies if the 1166 ** --technote option is also specified. 1167 ** 1168 ** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in 1169 ** year-month-day form, it may be truncated, the "T" may be replaced by 1170 ** a space, and it may also name a timezone offset from UTC as "-HH:MM" 1171 ** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" 1172 ** means UTC. 1173 ** 1174 */ 1175 void wiki_cmd(void){ 1176 int n; 1177 db_find_and_open_repository(0, 0); 1178 if( g.argc<3 ){ 1179 goto wiki_cmd_usage; 1180 } 1181 n = strlen(g.argv[2]); 1182 if( n==0 ){ 1183 goto wiki_cmd_usage; 1184 } 1185 1186 if( strncmp(g.argv[2],"export",n)==0 ){ 1187 const char *zPageName; /* Name of the wiki page to export */ 1188 const char *zFile; /* Name of the output file (0=stdout) */ 1189 const char *zETime; /* The name of the technote to export */ 1190 int rid; /* Artifact ID of the wiki page */ 1191 int i; /* Loop counter */ 1192 char *zBody = 0; /* Wiki page content */ 1193 Blob body; /* Wiki page content */ 1194 Manifest *pWiki = 0; /* Parsed wiki page content */ 1195 1196 zETime = find_option("technote","t",1); 1197 if( !zETime ){ 1198 if( (g.argc!=4) && (g.argc!=5) ){ 1199 usage("export PAGENAME ?FILE?"); 1200 } 1201 zPageName = g.argv[3]; 1202 rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" 1203 " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" 1204 " ORDER BY x.mtime DESC LIMIT 1", 1205 zPageName 1206 ); 1207 if( (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 ){ 1208 zBody = pWiki->zWiki; 1209 } 1210 if( zBody==0 ){ 1211 fossil_fatal("wiki page [%s] not found",zPageName); 1212 } 1213 zFile = (g.argc==4) ? "-" : g.argv[4]; 1214 }else{ 1215 if( (g.argc!=3) && (g.argc!=4) ){ 1216 usage("export ?FILE? --technote DATETIME|TECHNOTE-ID"); 1217 } 1218 rid = wiki_technote_to_rid(zETime); 1219 if ( rid==-1 ){ 1220 fossil_fatal("ambiguous tech note id: %s", zETime); 1221 } 1222 if( (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 ){ 1223 zBody = pWiki->zWiki; 1224 } 1225 if( zBody==0 ){ 1226 fossil_fatal("technote [%s] not found",zETime); 1227 } 1228 zFile = (g.argc==3) ? "-" : g.argv[3]; 1229 } 1230 for(i=strlen(zBody); i>0 && fossil_isspace(zBody[i-1]); i--){} 1231 zBody[i] = 0; 1232 blob_init(&body, zBody, -1); 1233 blob_append(&body, "\n", 1); 1234 blob_write_to_file(&body, zFile); 1235 blob_reset(&body); 1236 manifest_destroy(pWiki); 1237 return; 1238 }else if( strncmp(g.argv[2],"commit",n)==0 1239 || strncmp(g.argv[2],"create",n)==0 ){ 1240 const char *zPageName; /* page name */ 1241 Blob content; /* Input content */ 1242 int rid = 0; 1243 Manifest *pWiki = 0; /* Parsed wiki page content */ 1244 const char *zMimeType = find_option("mimetype", "M", 1); 1245 const char *zETime = find_option("technote", "t", 1); 1246 const char *zTags = find_option("technote-tags", NULL, 1); 1247 const char *zClr = find_option("technote-bgcolor", NULL, 1); 1248 if( g.argc!=4 && g.argc!=5 ){ 1249 usage("commit|create PAGENAME ?FILE? [--mimetype TEXT-FORMAT]" 1250 " [--technote DATETIME] [--technote-tags TAGS]" 1251 " [--technote-bgcolor COLOR]"); 1252 } 1253 zPageName = g.argv[3]; 1254 if( g.argc==4 ){ 1255 blob_read_from_channel(&content, stdin, -1); 1256 }else{ 1257 blob_read_from_file(&content, g.argv[4], ExtFILE); 1258 } 1259 if( !zMimeType || !*zMimeType ){ 1260 /* Try to deduce the mime type based on the prior version. */ 1261 if ( !zETime ){ 1262 rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" 1263 " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" 1264 " ORDER BY x.mtime DESC LIMIT 1", 1265 zPageName 1266 ); 1267 if( rid>0 && (pWiki = manifest_get(rid, CFTYPE_WIKI, 0))!=0 1268 && (pWiki->zMimetype && *pWiki->zMimetype) ){ 1269 zMimeType = pWiki->zMimetype; 1270 } 1271 }else{ 1272 rid = wiki_technote_to_rid(zETime); 1273 if( rid>0 && (pWiki = manifest_get(rid, CFTYPE_EVENT, 0))!=0 1274 && (pWiki->zMimetype && *pWiki->zMimetype) ){ 1275 zMimeType = pWiki->zMimetype; 1276 } 1277 } 1278 }else{ 1279 zMimeType = wiki_filter_mimetypes(zMimeType); 1280 } 1281 if( g.argv[2][1]=='r' && rid>0 ){ 1282 if ( !zETime ){ 1283 fossil_fatal("wiki page %s already exists", zPageName); 1284 }else{ 1285 /* Creating a tech note with same timestamp is permitted 1286 and should create a new tech note */ 1287 rid = 0; 1288 } 1289 }else if( g.argv[2][1]=='o' && rid == 0 ){ 1290 if ( !zETime ){ 1291 fossil_fatal("no such wiki page: %s", zPageName); 1292 }else{ 1293 fossil_fatal("no such tech note: %s", zETime); 1294 } 1295 } 1296 1297 if( !zETime ){ 1298 wiki_cmd_commit(zPageName, rid, &content, zMimeType, 1); 1299 if( g.argv[2][1]=='r' ){ 1300 fossil_print("Created new wiki page %s.\n", zPageName); 1301 }else{ 1302 fossil_print("Updated wiki page %s.\n", zPageName); 1303 } 1304 }else{ 1305 if( rid != -1 ){ 1306 char *zMETime; /* Normalized, mutable version of zETime */ 1307 zMETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", 1308 zETime); 1309 event_cmd_commit(zMETime, rid, &content, zMimeType, zPageName, 1310 zTags, zClr); 1311 if( g.argv[2][1]=='r' ){ 1312 fossil_print("Created new tech note %s.\n", zMETime); 1313 }else{ 1314 fossil_print("Updated tech note %s.\n", zMETime); 1315 } 1316 free(zMETime); 1317 }else{ 1318 fossil_fatal("ambiguous tech note id: %s", zETime); 1319 } 1320 } 1321 manifest_destroy(pWiki); 1322 blob_reset(&content); 1323 }else if( strncmp(g.argv[2],"delete",n)==0 ){ 1324 if( g.argc!=5 ){ 1325 usage("delete PAGENAME"); 1326 } 1327 fossil_fatal("delete not yet implemented."); 1328 }else if(( strncmp(g.argv[2],"list",n)==0 ) 1329 || ( strncmp(g.argv[2],"ls",n)==0 )){ 1330 Stmt q; 1331 int showIds = 0; 1332 1333 if ( !find_option("technote","t",0) ){ 1334 db_prepare(&q, 1335 "SELECT substr(tagname, 6) FROM tag WHERE tagname GLOB 'wiki-*'" 1336 " ORDER BY lower(tagname) /*sort*/" 1337 ); 1338 }else{ 1339 showIds = find_option("show-technote-ids","s",0)!=0; 1340 db_prepare(&q, 1341 "SELECT datetime(e.mtime), substr(t.tagname,7)" 1342 " FROM event e, tag t" 1343 " WHERE e.type='e'" 1344 " AND e.tagid IS NOT NULL" 1345 " AND t.tagid=e.tagid" 1346 " ORDER BY e.mtime DESC /*sort*/" 1347 ); 1348 } 1349 1350 while( db_step(&q)==SQLITE_ROW ){ 1351 const char *zName = db_column_text(&q, 0); 1352 if( showIds ){ 1353 const char *zUuid = db_column_text(&q, 1); 1354 fossil_print("%s ",zUuid); 1355 } 1356 fossil_print( "%s\n",zName); 1357 } 1358 db_finalize(&q); 1359 }else{ 1360 goto wiki_cmd_usage; 1361 } 1362 return; 1363 1364 wiki_cmd_usage: 1365 usage("export|create|commit|list ..."); 1366 } 1367 1368 /* 1369 ** COMMAND: test-markdown-render 1370 ** 1371 ** Usage: %fossil test-markdown-render FILE 1372 ** 1373 ** Render markdown wiki from FILE to stdout. 1374 ** 1375 */ 1376 void test_markdown_render(void){ 1377 Blob in, out; 1378 verify_all_options(); 1379 if( g.argc!=3 ) usage("FILE"); 1380 blob_zero(&out); 1381 blob_read_from_file(&in, g.argv[2], ExtFILE); 1382 markdown_to_html(&in, 0, &out); 1383 blob_write_to_file(&out, "-"); 1384 }