Fossil

Documentation
Login

Documentation

/*
** 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&nbsp;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.");
  }
}