diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/event.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/event.c')
| -rw-r--r-- | src/event.c | 620 |
1 files changed, 620 insertions, 0 deletions
diff --git a/src/event.c b/src/event.c new file mode 100644 index 0000000..75ffb6a --- /dev/null +++ b/src/event.c @@ -0,0 +1,620 @@ +/* +** 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 = rid_to_uuid(rid); + zFullId = db_text(0, "SELECT SUBSTR(tagname,7)" + " FROM tag" + " WHERE tagname GLOB 'event-%q*'", + zId); + 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?target=%s&from=%R/technote/%!S", zFullId, 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> + } + { + char * z = mprintf( + "<h2><a href='%R/attachlist?technote=%t'>Attachments</a>:</h2>", + zFullId + ); + attachment_list(zFullId, z, 1); + fossil_free(z); + } + 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 = fossil_strdup(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 default 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."); + } +} |
