/* ** Copyright (c) 2010 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains code to do formatting of event messages: ** ** Technical Notes ** Milestones ** Blog posts ** New articles ** Process checkpoints ** Announcements ** ** Do not confuse "event" artifacts with the "event" table in the ** repository database. An "event" artifact is a technical-note: a ** wiki- or blog-like essay that appears on the timeline. The "event" ** table records all entries on the timeline, including tech-notes. ** ** (2015-02-14): Changing the name to "tech-note" most everywhere. */ #include "config.h" #include <assert.h> #include <ctype.h> #include "event.h" /* ** Output a hyperlink to an technote given its tagid. */ void hyperlink_to_event_tagid(int tagid){ char *zId; zId = db_text(0, "SELECT substr(tagname, 7) FROM tag WHERE tagid=%d", tagid); @ [%z(href("%R/technote/%s",zId))%S(zId)</a>] free(zId); } /* ** WEBPAGE: technote ** WEBPAGE: event ** ** Display a technical note (formerly called an "event"). ** ** PARAMETERS: ** ** name=ID Identify the technical note to display. ID must be ** complete. ** aid=ARTIFACTID Which specific version of the tech-note. Optional. ** v=BOOLEAN Show details if TRUE. Default is FALSE. Optional. ** ** Display an existing tech-note identified by its ID, optionally at a ** specific version, and optionally with additional details. */ void event_page(void){ int rid = 0; /* rid of the event artifact */ char *zUuid; /* artifact hash corresponding to rid */ const char *zId; /* Event identifier */ const char *zVerbose; /* Value of verbose option */ char *zETime; /* Time of the tech-note */ char *zATime; /* Time the artifact was created */ int specRid; /* rid specified by aid= parameter */ int prevRid, nextRid; /* Previous or next edits of this tech-note */ Manifest *pTNote; /* Parsed technote artifact */ Blob fullbody; /* Complete content of the technote body */ Blob title; /* Title extracted from the technote body */ Blob tail; /* Event body that comes after the title */ Stmt q1; /* Query to search for the technote */ int verboseFlag; /* True to show details */ const char *zMimetype = 0; /* Mimetype of the document */ const char *zFullId; /* Full event identifier */ /* wiki-read privilege is needed in order to read tech-notes. */ login_check_credentials(); if( !g.perm.RdWiki ){ login_needed(g.anon.RdWiki); return; } zId = P("name"); if( zId==0 ){ fossil_redirect_home(); return; } zUuid = (char*)P("aid"); specRid = zUuid ? uuid_to_rid(zUuid, 0) : 0; rid = nextRid = prevRid = 0; db_prepare(&q1, "SELECT rid FROM tagxref" " WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB 'event-%q*')" " ORDER BY mtime DESC", zId ); while( db_step(&q1)==SQLITE_ROW ){ nextRid = rid; rid = db_column_int(&q1, 0); if( specRid==0 || specRid==rid ){ if( db_step(&q1)==SQLITE_ROW ){ prevRid = db_column_int(&q1, 0); } break; } } db_finalize(&q1); style_set_current_feature("event"); if( rid==0 || (specRid!=0 && specRid!=rid) ){ style_header("No Such Tech-Note"); @ Cannot locate a technical note called <b>%h(zId)</b>. style_finish_page(); return; } zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); zVerbose = P("v"); if( !zVerbose ){ zVerbose = P("verbose"); } if( !zVerbose ){ zVerbose = P("detail"); /* deprecated */ } verboseFlag = (zVerbose!=0) && !is_false(zVerbose); /* Extract the event content. */ cgi_check_for_malice(); pTNote = manifest_get(rid, CFTYPE_EVENT, 0); if( pTNote==0 ){ fossil_fatal("Object #%d is not a tech-note", rid); } zMimetype = wiki_filter_mimetypes(PD("mimetype",pTNote->zMimetype)); blob_init(&fullbody, pTNote->zWiki, -1); blob_init(&title, 0, 0); blob_init(&tail, 0, 0); if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ if( !wiki_find_title(&fullbody, &title, &tail) ){ blob_appendf(&title, "Tech-note %S", zId); tail = fullbody; } }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ markdown_to_html(&fullbody, &title, &tail); if( blob_size(&title)==0 ){ blob_appendf(&title, "Tech-note %S", zId); } }else{ blob_appendf(&title, "Tech-note %S", zId); tail = fullbody; } style_header("%s", blob_str(&title)); if( g.perm.WrWiki && g.perm.Write && nextRid==0 ){ style_submenu_element("Edit", "%R/technoteedit?name=%!S", zId); if( g.perm.Attach ){ style_submenu_element("Attach", "%R/attachadd?technote=%!S&from=%R/technote/%!S", zId, zId); } } zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); style_submenu_element("Context", "%R/timeline?c=%.20s", zId); if( g.perm.Hyperlink ){ if( verboseFlag ){ style_submenu_element("Plain", "%R/technote?name=%!S&aid=%s&mimetype=text/plain", zId, zUuid); if( nextRid ){ char *zNext; zNext = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", nextRid); style_submenu_element("Next", "%R/technote?name=%!S&aid=%s&v", zId, zNext); free(zNext); } if( prevRid ){ char *zPrev; zPrev = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", prevRid); style_submenu_element("Prev", "%R/technote?name=%!S&aid=%s&v", zId, zPrev); free(zPrev); } }else{ style_submenu_element("Detail", "%R/technote?name=%!S&aid=%s&v", zId, zUuid); } } if( verboseFlag && g.perm.Hyperlink ){ int i; const char *zClr = 0; Blob comment; zATime = db_text(0, "SELECT datetime(%.17g)", pTNote->rDate); @ <p>Tech-note [%z(href("%R/artifact/%!S",zUuid))%S(zUuid)</a>] at @ [%z(href("%R/timeline?c=%T",zETime))%s(zETime)</a>] @ entered by user <b>%h(pTNote->zUser)</b> on @ [%z(href("%R/timeline?c=%T",zATime))%s(zATime)</a>]:</p> @ <blockquote> for(i=0; i<pTNote->nTag; i++){ if( fossil_strcmp(pTNote->aTag[i].zName,"+bgcolor")==0 ){ zClr = pTNote->aTag[i].zValue; } } if( zClr && zClr[0]==0 ) zClr = 0; if( zClr ){ @ <div style="background-color: %h(zClr);"> }else{ @ <div> } blob_init(&comment, pTNote->zComment, -1); wiki_convert(&comment, 0, WIKI_INLINE); blob_reset(&comment); @ </div> @ </blockquote><hr> } if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ wiki_convert(&fullbody, 0, 0); }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ cgi_append_content(blob_buffer(&tail), blob_size(&tail)); }else{ @ <pre> @ %h(blob_str(&fullbody)) @ </pre> } zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" " FROM tag" " WHERE tagname GLOB 'event-%q*'", zId); attachment_list(zFullId, "<hr><h2>Attachments:</h2><ul>"); document_emit_js(); style_finish_page(); manifest_destroy(pTNote); } /* ** Add or update a new tech note to the repository. rid is id of ** the prior version of this technote, if any. ** ** returns 1 if the tech note was added or updated, 0 if the ** update failed making an invalid artifact */ int event_commit_common( int rid, /* id of the prior version of the technote */ const char *zId, /* hash label for the technote */ const char *zBody, /* content of the technote */ char *zETime, /* timestamp for the technote */ const char *zMimetype, /* mimetype for the technote N-card */ const char *zComment, /* comment shown on the timeline */ const char *zTags, /* tags associated with this technote */ const char *zClr /* Background color */ ){ Blob event; char *zDate; Blob cksum; int nrid, n; blob_init(&event, 0, 0); db_begin_transaction(); while( fossil_isspace(zComment[0]) ) zComment++; n = strlen(zComment); while( n>0 && fossil_isspace(zComment[n-1]) ){ n--; } if( n>0 ){ blob_appendf(&event, "C %#F\n", n, zComment); } zDate = date_in_standard_format("now"); blob_appendf(&event, "D %s\n", zDate); free(zDate); zETime[10] = 'T'; blob_appendf(&event, "E %s %s\n", zETime, zId); zETime[10] = ' '; if( zMimetype && zMimetype[0] ){ blob_appendf(&event, "N %s\n", zMimetype); } if( rid ){ char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid); blob_appendf(&event, "P %s\n", zUuid); free(zUuid); } if( zClr && zClr[0] ){ blob_appendf(&event, "T +bgcolor * %F\n", zClr); } if( zTags && zTags[0] ){ Blob tags, one; int i, j; Stmt q; char *zBlob; /* Load the tags string into a blob */ blob_zero(&tags); blob_append(&tags, zTags, -1); /* Collapse all sequences of whitespace and "," characters into ** a single space character */ zBlob = blob_str(&tags); for(i=j=0; zBlob[i]; i++, j++){ if( fossil_isspace(zBlob[i]) || zBlob[i]==',' ){ while( fossil_isspace(zBlob[i+1]) ){ i++; } zBlob[j] = ' '; }else{ zBlob[j] = zBlob[i]; } } blob_resize(&tags, j); /* Parse out each tag and load it into a temporary table for sorting */ db_multi_exec("CREATE TEMP TABLE newtags(x);"); while( blob_token(&tags, &one) ){ db_multi_exec("INSERT INTO newtags VALUES(%B)", &one); } blob_reset(&tags); /* Extract the tags in sorted order and make an entry in the ** artifact for each. */ db_prepare(&q, "SELECT x FROM newtags ORDER BY x"); while( db_step(&q)==SQLITE_ROW ){ blob_appendf(&event, "T +sym-%F *\n", db_column_text(&q, 0)); } db_finalize(&q); } if( !login_is_nobody() ){ blob_appendf(&event, "U %F\n", login_name()); } blob_appendf(&event, "W %d\n%s\n", strlen(zBody), zBody); md5sum_blob(&event, &cksum); blob_appendf(&event, "Z %b\n", &cksum); blob_reset(&cksum); nrid = content_put(&event); db_add_unsent(nrid); if( manifest_crosslink(nrid, &event, MC_NONE)==0 ){ db_end_transaction(1); return 0; } assert( blob_is_reset(&event) ); content_deltify(rid, &nrid, 1, 0); db_end_transaction(0); return 1; } /* ** WEBPAGE: technoteedit ** WEBPAGE: eventedit ** ** Revise or create a technical note (formerly called an "event"). ** ** Required query parameter: ** ** name=ID Hex hash ID of the technote. If omitted, a new ** tech-note is created. ** ** POST parameters from the "Cancel", "Preview", or "Submit" buttons: ** ** w=TEXT Complete text of the technote. ** t=TEXT Time of the technote on the timeline (ISO 8601) ** c=TEXT Timeline comment ** g=TEXT Tags associated with this technote ** mimetype=TEXT Mimetype for w= text ** newclr Use a background color ** clr=TEXT Background color to use if newclr ** ** For GET requests, when editing an existing technote newclr and clr ** are implied if a custom color has been set on the previous version ** of the technote. */ void eventedit_page(void){ char *zTag; int rid = 0; Blob event; const char *zId; int n; const char *z; char *zBody = (char*)P("w"); /* Text of the technote */ char *zETime = (char*)P("t"); /* Date this technote appears */ const char *zComment = P("c"); /* Timeline comment */ const char *zTags = P("g"); /* Tags added to this technote */ const char *zClrFlag = ""; /* "checked" for bg color */ const char *zClr; /* Name of the background color */ const char *zMimetype = P("mimetype"); /* Mimetype of zBody */ int isNew = 0; if( zBody ){ zBody = mprintf("%s", zBody); } login_check_credentials(); zId = P("name"); if( zId==0 ){ zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); isNew = 1; }else{ int nId = strlen(zId); if( !validate16(zId, nId) ){ fossil_redirect_home(); return; } } zTag = mprintf("event-%s", zId); rid = db_int(0, "SELECT rid FROM tagxref" " WHERE tagid=(SELECT tagid FROM tag WHERE tagname GLOB '%q*')" " ORDER BY mtime DESC", zTag ); if( rid && strlen(zId)<HNAME_MIN ){ zId = db_text(0, "SELECT substr(tagname,7) FROM tag WHERE tagname GLOB '%q*'", zTag ); } free(zTag); /* Need both check-in and wiki-write or wiki-create privileges in order ** to edit/create an event. */ if( !g.perm.Write || (rid && !g.perm.WrWiki) || (!rid && !g.perm.NewWiki) ){ login_needed(g.anon.Write && (rid ? g.anon.WrWiki : g.anon.NewWiki)); return; } style_set_current_feature("event"); /* Figure out the color */ if( rid ){ zClr = db_text("", "SELECT bgcolor FROM event WHERE objid=%d", rid); if( zClr && zClr[0] ){ const char * zRequestMethod = P("REQUEST_METHOD"); if(zRequestMethod && 'G'==zRequestMethod[0]){ /* Apply saved color by defaut for GET requests ** (e.g., an Edit menu link). */ zClrFlag = " checked"; } } }else{ zClr = ""; isNew = 1; } if( P("newclr") ){ zClr = PD("clr",zClr); if( zClr[0] ) zClrFlag = " checked"; } /* If editing an existing event, extract the key fields to use as ** a starting point for the edit. */ if( rid && (zBody==0 || zETime==0 || zComment==0 || zTags==0 || zMimetype==0) ){ Manifest *pTNote; pTNote = manifest_get(rid, CFTYPE_EVENT, 0); if( pTNote && pTNote->type==CFTYPE_EVENT ){ if( zBody==0 ) zBody = pTNote->zWiki; if( zETime==0 ){ zETime = db_text(0, "SELECT datetime(%.17g)", pTNote->rEventDate); } if( zComment==0 ) zComment = pTNote->zComment; if( zMimetype==0 ) zMimetype = pTNote->zMimetype; } if( zTags==0 ){ zTags = db_text(0, "SELECT group_concat(substr(tagname,5),', ')" " FROM tagxref, tag" " WHERE tagxref.rid=%d" " AND tagxref.tagid=tag.tagid" " AND tag.tagname GLOB 'sym-*'", rid ); } } zETime = db_text(0, "SELECT coalesce(datetime(%Q),datetime('now'))", zETime); if( P("submit")!=0 && (zBody!=0 && zComment!=0) && cgi_csrf_safe(2) ){ if ( !event_commit_common(rid, zId, zBody, zETime, zMimetype, zComment, zTags, zClrFlag[0] ? zClr : 0) ){ style_header("Error"); @ Internal error: Fossil tried to make an invalid artifact for @ the edited technote. style_finish_page(); return; } cgi_redirectf("%R/technote?name=%T", zId); } if( P("cancel")!=0 ){ cgi_redirectf("%R/technote?name=%T", zId); return; } if( zBody==0 ){ zBody = mprintf("Insert new content here..."); } if( isNew ){ style_header("New Tech-note %S", zId); }else{ style_header("Edit Tech-note %S", zId); } if( P("preview")!=0 ){ Blob com; @ <p><b>Timeline comment preview:</b></p> @ <blockquote> @ <table border="0"> if( zClrFlag[0] && zClr && zClr[0] ){ @ <tr><td style="background-color: %h(zClr);"> }else{ @ <tr><td> } blob_zero(&com); blob_append(&com, zComment, -1); wiki_convert(&com, 0, WIKI_INLINE|WIKI_NOBADLINKS); @ </td></tr></table> @ </blockquote> @ <p><b>Page content preview:</b><p> @ <blockquote> blob_init(&event, 0, 0); blob_append(&event, zBody, -1); safe_html_context(DOCSRC_WIKI); wiki_render_by_mimetype(&event, zMimetype); @ </blockquote><hr> blob_reset(&event); } for(n=2, z=zBody; z[0]; z++){ if( z[0]=='\n' ) n++; } if( n<20 ) n = 20; if( n>40 ) n = 40; @ <form method="post" action="%R/technoteedit"><div> login_insert_csrf_secret(); @ <input type="hidden" name="name" value="%h(zId)"> @ <table border="0" cellspacing="10"> @ <tr><th align="right" valign="top">Timestamp (UTC):</th> @ <td valign="top"> @ <input type="text" name="t" size="25" value="%h(zETime)"> @ </td></tr> @ <tr><th align="right" valign="top">Timeline Comment:</th> @ <td valign="top"> @ <textarea name="c" class="technoteedit" cols="80" @ rows="3" wrap="virtual">%h(zComment)</textarea> @ </td></tr> @ <tr><th align="right" valign="top">Timeline Background Color:</th> @ <td valign="top"> @ <input type='checkbox' name='newclr'%s(zClrFlag)> @ Use custom color: \ @ <input type='color' name='clr' value='%s(zClr[0]?zClr:"#c0f0ff")'> @ </td></tr> @ <tr><th align="right" valign="top">Tags:</th> @ <td valign="top"> @ <input type="text" name="g" size="40" value="%h(zTags)"> @ </td></tr> @ <tr><th align="right" valign="top">\ @ %z(href("%R/markup_help"))Markup Style</a>:</th> @ <td valign="top"> mimetype_option_menu(zMimetype, "mimetype"); @ </td></tr> @ <tr><th align="right" valign="top">Page Content:</th> @ <td valign="top"> @ <textarea name="w" class="technoteedit" cols="80" @ rows="%d(n)" wrap="virtual">%h(zBody)</textarea> @ </td></tr> @ <tr><td colspan="2"> @ <input type="submit" name="cancel" value="Cancel"> @ <input type="submit" name="preview" value="Preview"> if( P("preview") ){ @ <input type="submit" name="submit" value="Submit"> } @ </td></tr></table> @ </div></form> style_finish_page(); } /* ** Add a new tech note to the repository. The timestamp is ** given by the zETime parameter. rid must be zero to create ** a new page. If no previous page with the name zPageName exists ** and isNew is false, then this routine throws an error. */ void event_cmd_commit( char *zETime, /* timestamp */ int rid, /* Artifact id of the tech note */ Blob *pContent, /* content of the new page */ const char *zMimeType, /* mimetype of the content */ const char *zComment, /* comment to go on the timeline */ const char *zTags, /* tags */ const char *zClr /* background color */ ){ const char *zId; /* id of the tech note */ if ( rid==0 ){ zId = db_text(0, "SELECT lower(hex(randomblob(20)))"); }else{ zId = db_text(0, "SELECT substr(tagname,7) FROM tag" " WHERE tagid=(SELECT tagid FROM event WHERE objid='%d')", rid ); } user_select(); if (event_commit_common(rid, zId, blob_str(pContent), zETime, zMimeType, zComment, zTags, zClr)==0 ){ #ifdef FOSSIL_ENABLE_JSON g.json.resultCode = FSL_JSON_E_ASSERT; #endif fossil_panic("Internal error: Fossil tried to make an " "invalid artifact for the technote."); } }