Fossil
Check-in [f4a25366a7]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview

SHA1 Hash:f4a25366a76d89dce3dc191246bbea3babb53505
Date: 2010-03-18 13:26:36
User: drh
Comment:Merge recent experimental changes (the attachment enhancement and the ability to delete wiki) into the trunk.

Tags And Properties
Changes
[hide diffs]

Added src/attach.c

Changes to src/info.c

@@ -732,10 +732,52 @@
       }
       cnt++;
     }
     db_finalize(&q);
   }
+  db_prepare(&q,
+    "SELECT target, filename, datetime(mtime), user, src"
+    "  FROM attachment"
+    " WHERE src=(SELECT uuid FROM blob WHERE rid=%d)"
+    " ORDER BY mtime DESC",
+    rid
+  );
+  while( db_step(&q)==SQLITE_ROW ){
+    const char *zTarget = db_column_text(&q, 0);
+    const char *zFilename = db_column_text(&q, 1);
+    const char *zDate = db_column_text(&q, 2);
+    const char *zUser = db_column_text(&q, 3);
+    const char *zSrc = db_column_text(&q, 4);
+    if( cnt>0 ){
+      @ Also attachment "%h(zFilename)" to
+    }else{
+      @ Attachment "%h(zFilename)" to
+    }
+    if( strlen(zTarget)==UUID_SIZE && validate16(zTarget,UUID_SIZE) ){
+      char zShort[20];
+      memcpy(zShort, zTarget, 10);
+      if( g.okHistory && g.okRdTkt ){
+        @ ticket [<a href="%s(g.zTop)/tktview?name=%s(zShort)">%s(zShort)</a>]
+      }else{
+        @ ticket [%s(zShort)]
+      }
+    }else{
+      if( g.okHistory && g.okRdWiki ){
+        @ wiki page [<a href="%s(g.zTop)/wiki?name=%t(zTarget)">%h(zTarget)</a>]
+      }else{
+        @ wiki page [%h(zTarget)]
+      }
+    }
+    @ added by
+    hyperlink_to_user(zUser,zDate," on");
+    hyperlink_to_date(zDate,".");
+    cnt++;
+    if( pDownloadName && blob_size(pDownloadName)==0 ){
+      blob_append(pDownloadName, zSrc, -1);
+    }
+  }
+  db_finalize(&q);
   if( cnt==0 ){
     char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
     @ Control artifact.
     if( pDownloadName && blob_size(pDownloadName)==0 ){
       blob_append(pDownloadName, zUuid, -1);

Changes to src/login.c

@@ -476,11 +476,12 @@
       case 's':   g.okSetup = 1;  /* Fall thru into Admin */
       case 'a':   g.okAdmin = g.okRdTkt = g.okWrTkt = g.okZip =
                               g.okRdWiki = g.okWrWiki = g.okNewWiki =
                               g.okApndWiki = g.okHistory = g.okClone =
                               g.okNewTkt = g.okPassword = g.okRdAddr =
-                              g.okTktFmt = 1;  /* Fall thru into Read/Write */
+                              g.okTktFmt = g.okAttach = 1;
+                              /* Fall thru into Read/Write */
       case 'i':   g.okRead = g.okWrite = 1;                     break;
       case 'o':   g.okRead = 1;                                 break;
       case 'z':   g.okZip = 1;                                  break;
 
       case 'd':   g.okDelete = 1;                               break;
@@ -498,10 +499,11 @@
       case 'n':   g.okNewTkt = 1;                               break;
       case 'w':   g.okWrTkt = g.okRdTkt = g.okNewTkt =
                   g.okApndTkt = 1;                              break;
       case 'c':   g.okApndTkt = 1;                              break;
       case 't':   g.okTktFmt = 1;                               break;
+      case 'b':   g.okAttach = 1;                               break;
 
       /* The "u" privileges is a little different.  It recursively
       ** inherits all privileges of the user named "reader" */
       case 'u': {
         if( zUser==0 ){
@@ -534,11 +536,11 @@
   int rc = 1;
   if( nCap<0 ) nCap = strlen(zCap);
   for(i=0; i<nCap && rc && zCap[i]; i++){
     switch( zCap[i] ){
       case 'a':  rc = g.okAdmin;     break;
-      /* case 'b': */
+      case 'b':  rc = g.okAttach;    break;
       case 'c':  rc = g.okApndTkt;   break;
       case 'd':  rc = g.okDelete;    break;
       case 'e':  rc = g.okRdAddr;    break;
       case 'f':  rc = g.okNewWiki;   break;
       case 'g':  rc = g.okClone;     break;

Changes to src/main.c

@@ -130,10 +130,11 @@
   int okWrWiki;           /* k: edit wiki via web */
   int okRdTkt;            /* r: view tickets via web */
   int okNewTkt;           /* n: create new tickets */
   int okApndTkt;          /* c: append to tickets via the web */
   int okWrTkt;            /* w: make changes to tickets via web */
+  int okAttach;           /* b: add attachments */
   int okTktFmt;           /* t: create new ticket report formats */
   int okRdAddr;           /* e: read email addresses or other private data */
   int okZip;              /* z: download zipped artifact via /zip URL */
 
   /* For defense against Cross-site Request Forgery attacks */

Changes to src/main.mk

@@ -13,10 +13,11 @@
 
 
 SRC = \
   $(SRCDIR)/add.c \
   $(SRCDIR)/allrepo.c \
+  $(SRCDIR)/attach.c \
   $(SRCDIR)/bag.c \
   $(SRCDIR)/blob.c \
   $(SRCDIR)/branch.c \
   $(SRCDIR)/browse.c \
   $(SRCDIR)/captcha.c \
@@ -86,10 +87,11 @@
   $(SRCDIR)/zip.c
 
 TRANS_SRC = \
   add_.c \
   allrepo_.c \
+  attach_.c \
   bag_.c \
   blob_.c \
   branch_.c \
   browse_.c \
   captcha_.c \
@@ -159,10 +161,11 @@
   zip_.c
 
 OBJ = \
  $(OBJDIR)/add.o \
  $(OBJDIR)/allrepo.o \
+ $(OBJDIR)/attach.o \
  $(OBJDIR)/bag.o \
  $(OBJDIR)/blob.o \
  $(OBJDIR)/branch.o \
  $(OBJDIR)/browse.o \
  $(OBJDIR)/captcha.o \
@@ -273,16 +276,16 @@
 	# noop
 
 clean:
 	rm -f $(OBJDIR)/*.o *_.c $(APPNAME) VERSION.h
 	rm -f translate makeheaders mkindex page_index.h headers
-	rm -f add.h allrepo.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h finfo.h graph.h http.h http_socket.h http_ssl.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h search.h setup.h sha1.h shun.h skins.h stat.h style.h sync.h tag.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
+	rm -f add.h allrepo.h attach.h bag.h blob.h branch.h browse.h captcha.h cgi.h checkin.h checkout.h clearsign.h clone.h comformat.h configure.h construct.h content.h db.h delta.h deltacmd.h descendants.h diff.h diffcmd.h doc.h encode.h file.h finfo.h graph.h http.h http_socket.h http_ssl.h http_transport.h info.h login.h main.h manifest.h md5.h merge.h merge3.h name.h pivot.h pqueue.h printf.h rebuild.h report.h rss.h rstats.h schema.h search.h setup.h sha1.h shun.h skins.h stat.h style.h sync.h tag.h th_main.h timeline.h tkt.h tktsetup.h undo.h update.h url.h user.h verify.h vfile.h wiki.h wikiformat.h winhttp.h xfer.h zip.h
 
 page_index.h: $(TRANS_SRC) mkindex
 	./mkindex $(TRANS_SRC) >$@
 headers:	page_index.h makeheaders VERSION.h
-	./makeheaders  add_.c:add.h allrepo_.c:allrepo.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h finfo_.c:finfo.h graph_.c:graph.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h search_.c:search.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h skins_.c:skins.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
+	./makeheaders  add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h bag_.c:bag.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h construct_.c:construct.h content_.c:content.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h doc_.c:doc.h encode_.c:encode.h file_.c:file.h finfo_.c:finfo.h graph_.c:graph.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h info_.c:info.h login_.c:login.h main_.c:main.h manifest_.c:manifest.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h name_.c:name.h pivot_.c:pivot.h pqueue_.c:pqueue.h printf_.c:printf.h rebuild_.c:rebuild.h report_.c:report.h rss_.c:rss.h rstats_.c:rstats.h schema_.c:schema.h search_.c:search.h setup_.c:setup.h sha1_.c:sha1.h shun_.c:shun.h skins_.c:skins.h stat_.c:stat.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h update_.c:update.h url_.c:url.h user_.c:user.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winhttp_.c:winhttp.h xfer_.c:xfer.h zip_.c:zip.h $(SRCDIR)/sqlite3.h $(SRCDIR)/th.h VERSION.h
 	touch headers
 headers: Makefile
 Makefile:
 add_.c:	$(SRCDIR)/add.c translate
 	./translate $(SRCDIR)/add.c >add_.c
@@ -296,10 +299,17 @@
 
 $(OBJDIR)/allrepo.o:	allrepo_.c allrepo.h  $(SRCDIR)/config.h
 	$(XTCC) -o $(OBJDIR)/allrepo.o -c allrepo_.c
 
 allrepo.h:	headers
+attach_.c:	$(SRCDIR)/attach.c translate
+	./translate $(SRCDIR)/attach.c >attach_.c
+
+$(OBJDIR)/attach.o:	attach_.c attach.h  $(SRCDIR)/config.h
+	$(XTCC) -o $(OBJDIR)/attach.o -c attach_.c
+
+attach.h:	headers
 bag_.c:	$(SRCDIR)/bag.c translate
 	./translate $(SRCDIR)/bag.c >bag_.c
 
 $(OBJDIR)/bag.o:	bag_.c bag.h  $(SRCDIR)/config.h
 	$(XTCC) -o $(OBJDIR)/bag.o -c bag_.c

Changes to src/makemake.tcl

@@ -7,10 +7,11 @@
 # "translate" and "makeheaders"
 #
 set src {
   add
   allrepo
+  attach
   bag
   blob
   branch
   browse
   captcha

Changes to src/manifest.c

@@ -196,11 +196,11 @@
         }
         if( blob_size(&a3)>0
          && (blob_size(&a3)!=UUID_SIZE || !validate16(zSrc, UUID_SIZE)) ){
           goto manifest_syntax_error;
         }
-        p->zAttachName = zName;
+        p->zAttachName = (char*)file_tail(zName);
         p->zAttachSrc = zSrc;
         p->zAttachTarget = zTarget;
         break;
       }
 
@@ -1080,11 +1080,16 @@
   if( m.type==CFTYPE_WIKI ){
     char *zTag = mprintf("wiki-%s", m.zWikiTitle);
     int tagid = tag_findid(zTag, 1);
     int prior;
     char *zComment;
-    tag_insert(zTag, 1, 0, rid, m.rDate, rid);
+    int nWiki;
+    char zLength[40];
+    while( isspace(m.zWiki[0]) ) m.zWiki++;
+    nWiki = strlen(m.zWiki);
+    sqlite3_snprintf(sizeof(zLength), zLength, "%d", nWiki);
+    tag_insert(zTag, 1, zLength, rid, m.rDate, rid);
     free(zTag);
     prior = db_int(0,
       "SELECT rid FROM tagxref"
       " WHERE tagid=%d AND mtime<%.17g"
       " ORDER BY mtime DESC",
@@ -1091,11 +1096,15 @@
       tagid, m.rDate
     );
     if( prior ){
       content_deltify(prior, rid, 0);
     }
-    zComment = mprintf("Changes to wiki page [%h]", m.zWikiTitle);
+    if( nWiki>0 ){
+      zComment = mprintf("Changes to wiki page [%h]", m.zWikiTitle);
+    }else{
+      zComment = mprintf("Deleted wiki page [%h]", m.zWikiTitle);
+    }
     db_multi_exec(
       "REPLACE INTO event(type,mtime,objid,user,comment,"
       "                  bgcolor,euser,ecomment)"
       "VALUES('w',%.17g,%d,%Q,%Q,"
       "  (SELECT value FROM tagxref WHERE tagid=%d AND rid=%d AND tagtype>1),"
@@ -1119,20 +1128,57 @@
     db_multi_exec("INSERT OR IGNORE INTO pending_tkt VALUES(%Q)",
                   m.zTicketUuid);
   }
   if( m.type==CFTYPE_ATTACHMENT ){
     db_multi_exec(
-       "INSERT OR IGNORE INTO attachment(mtime, target, filename)"
-       "VALUES(0.0,%Q,%Q)",
-       m.zAttachTarget, m.zAttachName
+       "INSERT INTO attachment(attachid, mtime, src, target,"
+                                        "filename, comment, user)"
+       "VALUES(%d,%.17g,%Q,%Q,%Q,%Q,%Q);",
+       rid, m.rDate, m.zAttachSrc, m.zAttachTarget, m.zAttachName,
+       (m.zComment ? m.zComment : ""), m.zUser
     );
     db_multi_exec(
-       "UPDATE attachment SET mtime=%.17g, src=%Q, comment=%Q, user=%Q"
-       " WHERE mtime<%.17g AND target=%Q AND filename=%Q",
-       m.rDate, m.zAttachSrc, m.zComment, m.zUser,
-       m.rDate, m.zAttachTarget, m.zAttachName
+       "UPDATE attachment SET isLatest = (mtime=="
+          "(SELECT max(mtime) FROM attachment"
+          "  WHERE target=%Q AND filename=%Q))"
+       " WHERE target=%Q AND filename=%Q",
+       m.zAttachTarget, m.zAttachName,
+       m.zAttachTarget, m.zAttachName
     );
+    if( strlen(m.zAttachTarget)!=UUID_SIZE
+     || !validate16(m.zAttachTarget, UUID_SIZE)
+    ){
+      char *zComment;
+      if( m.zAttachSrc && m.zAttachSrc[0] ){
+        zComment = mprintf("Add attachment \"%h\" to wiki page [%h]",
+             m.zAttachName, m.zAttachTarget);
+      }else{
+        zComment = mprintf("Delete attachment \"%h\" from wiki page [%h]",
+             m.zAttachName, m.zAttachTarget);
+      }
+      db_multi_exec(
+        "REPLACE INTO event(type,mtime,objid,user,comment)"
+        "VALUES('w',%.17g,%d,%Q,%Q)",
+        m.rDate, rid, m.zUser, zComment
+      );
+      free(zComment);
+    }else{
+      char *zComment;
+      if( m.zAttachSrc && m.zAttachSrc[0] ){
+        zComment = mprintf("Add attachment \"%h\" to ticket [%.10s]",
+             m.zAttachName, m.zAttachTarget);
+      }else{
+        zComment = mprintf("Delete attachment \"%h\" from ticket [%.10s]",
+             m.zAttachName, m.zAttachTarget);
+      }
+      db_multi_exec(
+        "REPLACE INTO event(type,mtime,objid,user,comment)"
+        "VALUES('t',%.17g,%d,%Q,%Q)",
+        m.rDate, rid, m.zUser, zComment
+      );
+      free(zComment);
+    }
   }
   db_end_transaction(0);
   manifest_clear(&m);
   return 1;
 }

Changes to src/schema.c

@@ -327,18 +327,21 @@
 @
 @ -- Each attachment is an entry in the following table.  Only
 @ -- the most recent attachment (identified by the D card) is saved.
 @ --
 @ CREATE TABLE attachment(
+@   attachid INTEGER PRIMARY KEY,   -- Local id for this attachment
+@   isLatest BOOLEAN DEFAULT 0,     -- True if this is the one to use
 @   mtime TIMESTAMP,                -- Time when attachment last changed
 @   src TEXT,                       -- UUID of the attachment.  NULL to delete
-@   target TEXT,                    -- Object attached to
+@   target TEXT,                    -- Object attached to. Wikiname or Tkt UUID
 @   filename TEXT,                  -- Filename for the attachment
 @   comment TEXT,                   -- Comment associated with this attachment
-@   user TEXT,                      -- Name of user adding attachment
-@   PRIMARY KEY(target, filename)
+@   user TEXT                       -- Name of user adding attachment
 @ );
+@ CREATE INDEX attachment_idx1 ON attachment(target, filename, mtime);
+@ CREATE INDEX attachment_idx2 ON attachment(src);
 @
 @ -- Template for the TICKET table
 @ --
 @ -- NB: when changing the schema of the TICKET table here, also make the
 @ -- same change in tktsetup.c.

Changes to src/setup.c

@@ -147,10 +147,12 @@
   @ <ol>
   @ <li><p>The permission flags are as follows:</p>
   @ <table>
      @ <tr><td valign="top"><b>a</b></td>
      @   <td><i>Admin:</i> Create and delete users</td></tr>
+     @ <tr><td valign="top"><b>b</b></td>
+     @   <td><i>Attach:</i> Add attachments to wiki or tickets</td></tr>
      @ <tr><td valign="top"><b>c</b></td>
      @   <td><i>Append-Tkt:</i> Append to tickets</td></tr>
      @ <tr><td valign="top"><b>d</b></td>
      @   <td><i>Delete:</i> Delete wiki and tickets</td></tr>
      @ <tr><td valign="top"><b>e</b></td>
@@ -239,11 +241,11 @@
 */
 void user_edit(void){
   const char *zId, *zLogin, *zInfo, *zCap, *zPw;
   char *oaa, *oas, *oar, *oaw, *oan, *oai, *oaj, *oao, *oap;
   char *oak, *oad, *oac, *oaf, *oam, *oah, *oag, *oae;
-  char *oat, *oau, *oav, *oaz;
+  char *oat, *oau, *oav, *oab, *oaz;
   const char *inherit[128];
   int doWrite;
   int uid;
   int higherUser = 0;  /* True if user being edited is SETUP and the */
                        /* user doing the editing is ADMIN.  Disallow editing */
@@ -276,10 +278,11 @@
   doWrite = cgi_all("login","info","pw") && !higherUser;
   if( doWrite ){
     char zCap[50];
     int i = 0;
     int aa = P("aa")!=0;
+    int ab = P("ab")!=0;
     int ad = P("ad")!=0;
     int ae = P("ae")!=0;
     int ai = P("ai")!=0;
     int aj = P("aj")!=0;
     int ak = P("ak")!=0;
@@ -297,10 +300,11 @@
     int at = P("at")!=0;
     int au = P("au")!=0;
     int av = P("av")!=0;
     int az = P("az")!=0;
     if( aa ){ zCap[i++] = 'a'; }
+    if( ab ){ zCap[i++] = 'b'; }
     if( ac ){ zCap[i++] = 'c'; }
     if( ad ){ zCap[i++] = 'd'; }
     if( ae ){ zCap[i++] = 'e'; }
     if( af ){ zCap[i++] = 'f'; }
     if( ah ){ zCap[i++] = 'h'; }
@@ -353,18 +357,19 @@
   */
   zLogin = "";
   zInfo = "";
   zCap = "";
   zPw = "";
-  oaa = oac = oad = oae = oaf = oag = oah = oai = oaj = oak = oam =
+  oaa = oab = oac = oad = oae = oaf = oag = oah = oai = oaj = oak = oam =
         oan = oao = oap = oar = oas = oat = oau = oav = oaw = oaz = "";
   if( uid ){
     zLogin = db_text("", "SELECT login FROM user WHERE uid=%d", uid);
     zInfo = db_text("", "SELECT info FROM user WHERE uid=%d", uid);
     zCap = db_text("", "SELECT cap FROM user WHERE uid=%d", uid);
     zPw = db_text("", "SELECT pw FROM user WHERE uid=%d", uid);
     if( strchr(zCap, 'a') ) oaa = " checked";
+    if( strchr(zCap, 'b') ) oab = " checked";
     if( strchr(zCap, 'c') ) oac = " checked";
     if( strchr(zCap, 'd') ) oad = " checked";
     if( strchr(zCap, 'e') ) oae = " checked";
     if( strchr(zCap, 'f') ) oaf = " checked";
     if( strchr(zCap, 'g') ) oag = " checked";
@@ -467,15 +472,16 @@
   @    <input type="checkbox" name="ag"%s(oag)/>%s(B('g'))Clone<br>
   @    <input type="checkbox" name="aj"%s(oaj)/>%s(B('j'))Read Wiki<br>
   @    <input type="checkbox" name="af"%s(oaf)/>%s(B('f'))New Wiki<br>
   @    <input type="checkbox" name="am"%s(oam)/>%s(B('m'))Append Wiki<br>
   @    <input type="checkbox" name="ak"%s(oak)/>%s(B('k'))Write Wiki<br>
-  @    <input type="checkbox" name="ar"%s(oar)/>%s(B('r'))Read Tkt<br>
-  @    <input type="checkbox" name="an"%s(oan)/>%s(B('n'))New Tkt<br>
-  @    <input type="checkbox" name="ac"%s(oac)/>%s(B('c'))Append Tkt<br>
-  @    <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Tkt<br>
-  @    <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Tkt Report<br>
+  @    <input type="checkbox" name="ab"%s(oab)/>%s(B('b'))Attachments<br>
+  @    <input type="checkbox" name="ar"%s(oar)/>%s(B('r'))Read Ticket<br>
+  @    <input type="checkbox" name="an"%s(oan)/>%s(B('n'))New Ticket<br>
+  @    <input type="checkbox" name="ac"%s(oac)/>%s(B('c'))Append Ticket<br>
+  @    <input type="checkbox" name="aw"%s(oaw)/>%s(B('w'))Write Ticket<br>
+  @    <input type="checkbox" name="at"%s(oat)/>%s(B('t'))Ticket Report<br>
   @    <input type="checkbox" name="az"%s(oaz)/>%s(B('z'))Download Zip
   @   </td>
   @ </tr>
   @ <tr>
   @   <td align="right">Password:</td>
@@ -564,13 +570,13 @@
   @ </li><p>
   @
   @ <li><p>
   @ The <b>Read Wiki</b>, <b>New Wiki</b>, <b>Append Wiki</b>, and
   @ <b>Write Wiki</b> privileges control access to wiki pages.  The
-  @ <b>Read Tkt</b>, <b>New Tkt</b>, <b>Append Tkt</b>, and
-  @ <b>Write Tkt</b> privileges control access to trouble tickets.
-  @ The <b>Tkt Report</b> privilege allows the user to create or edit
+  @ <b>Read Ticket</b>, <b>New Ticket</b>, <b>Append Ticket</b>, and
+  @ <b>Write Ticket</b> privileges control access to trouble tickets.
+  @ The <b>Ticket Report</b> privilege allows the user to create or edit
   @ ticket report formats.
   @ </p></li>
   @
   @ <li><p>
   @ Users with the <b>Password</b> privilege are allowed to change their
@@ -582,10 +588,15 @@
   @ The <b>EMail</b> privilege allows the display of sensitive information
   @ such as the email address of users and contact information on tickets.
   @ Recommended OFF for "anonymous" and for "nobody" but ON for
   @ "developer".
   @ </p></li>
+  @
+  @ <li><p>
+  @ The <b>Attachment</b> privilege is needed in order to add attachments
+  @ to tickets or wiki.  Write privilege on the ticket or wiki is also
+  @ required.</p></li>
   @
   @ <li><p>
   @ Login is prohibited if the password is an empty string.
   @ </p></li>
   @ </ul>

Changes to src/tkt.c

@@ -296,18 +296,20 @@
 **
 ** View a ticket.
 */
 void tktview_page(void){
   const char *zScript;
+  char *zFullName;
+  const char *zUuid = PD("name","");
+
   login_check_credentials();
   if( !g.okRdTkt ){ login_needed(); return; }
   if( g.okWrTkt || g.okApndTkt ){
     style_submenu_element("Edit", "Edit The Ticket", "%s/tktedit?name=%T",
         g.zTop, PD("name",""));
   }
   if( g.okHistory ){
-    const char *zUuid = PD("name","");
     style_submenu_element("History", "History Of This Ticket",
         "%s/tkthistory/%T", g.zTop, zUuid);
     style_submenu_element("Timeline", "Timeline Of This Ticket",
         "%s/tkttimeline/%T", g.zTop, zUuid);
     style_submenu_element("Check-ins", "Check-ins Of This Ticket",
@@ -315,18 +317,58 @@
   }
   if( g.okNewTkt ){
     style_submenu_element("New Ticket", "Create a new ticket",
         "%s/tktnew", g.zTop);
   }
+  if( g.okApndTkt && g.okAttach ){
+    style_submenu_element("Attach", "Add An Attachment",
+        "%s/attachadd?tkt=%T&from=%s/tktview%%3fname=%t",
+        g.zTop, zUuid, g.zTop, zUuid);
+  }
   style_header("View Ticket");
   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW<br />\n", -1);
   ticket_init();
   initializeVariablesFromDb();
   zScript = ticket_viewpage_code();
   if( g.thTrace ) Th_Trace("BEGIN_TKTVIEW_SCRIPT<br />\n", -1);
   Th_Render(zScript);
   if( g.thTrace ) Th_Trace("END_TKTVIEW<br />\n", -1);
+
+  zFullName = db_text(0,
+       "SELECT tkt_uuid FROM ticket"
+       " WHERE tkt_uuid GLOB '%q*'", zUuid);
+  if( zFullName ){
+    int cnt = 0;
+    Stmt q;
+    db_prepare(&q,
+       "SELECT datetime(mtime,'localtime'), filename, user"
+       "  FROM attachment"
+       " WHERE isLatest AND src!='' AND target=%Q"
+       " ORDER BY mtime DESC",
+       zFullName);
+    while( db_step(&q)==SQLITE_ROW ){
+      const char *zDate = db_column_text(&q, 0);
+      const char *zFile = db_column_text(&q, 1);
+      const char *zUser = db_column_text(&q, 2);
+      if( cnt==0 ){
+        @ <hr><h2>Attachments:</h2>
+        @ <ul>
+      }
+      cnt++;
+      @ <li><a href="%s(g.zTop)/attachview?tkt=%s(zFullName)&file=%t(zFile)">
+      @ %h(zFile)</a> add by %h(zUser) on
+      hyperlink_to_date(zDate, ".");
+      if( g.okWrTkt && g.okAttach ){
+        @ [<a href="%s(g.zTop)/attachdelete?tkt=%s(zFullName)&file=%t(zFile)&from=%s(g.zTop)/tktview%%3fname=%s(zFullName)">delete</a>]
+      }
+    }
+    if( cnt ){
+      @ </ul>
+    }
+    db_finalize(&q);
+  }
+
   style_footer();
 }
 
 /*
 ** TH command:   append_field FIELD STRING
@@ -641,15 +683,18 @@
          timeline_query_for_www(), zFullUuid, zFullUuid
     );
   }else{
     zSQL = mprintf(
          "%s AND event.objid IN "
-         "  (SELECT rid FROM tagxref WHERE tagid=%d UNION"
-         "   SELECT srcid FROM backlink WHERE target GLOB '%.4s*' "
-                                         "AND '%s' GLOB (target||'*')) "
+         "  (SELECT rid FROM tagxref WHERE tagid=%d"
+         "   UNION SELECT srcid FROM backlink"
+                  " WHERE target GLOB '%.4s*'"
+                  "   AND '%s' GLOB (target||'*')"
+         "   UNION SELECT attachid FROM attachment"
+                  " WHERE target=%Q) "
          "ORDER BY mtime DESC",
-         timeline_query_for_www(), tagid, zFullUuid, zFullUuid
+         timeline_query_for_www(), tagid, zFullUuid, zFullUuid, zFullUuid
     );
   }
   db_prepare(&q, zSQL);
   free(zSQL);
   www_print_timeline(&q, TIMELINE_ARTID, 0);
@@ -687,37 +732,60 @@
     @ No such ticket: %h(zUuid)
     style_footer();
     return;
   }
   db_prepare(&q,
-    "SELECT objid, uuid FROM event, blob"
+    "SELECT datetime(mtime,'localtime'), objid, uuid, NULL, NULL, NULL"
+    "  FROM event, blob"
     " WHERE objid IN (SELECT rid FROM tagxref WHERE tagid=%d)"
     "   AND blob.rid=event.objid"
-    " ORDER BY mtime DESC",
-    tagid
+    " UNION "
+    "SELECT datetime(mtime,'localtime'), attachid, uuid, src, filename, user"
+    "  FROM attachment, blob"
+    " WHERE target=(SELECT substr(tagname,5) FROM tag WHERE tagid=%d)"
+    "   AND blob.rid=attachid"
+    " ORDER BY 1 DESC",
+    tagid, tagid
   );
   while( db_step(&q)==SQLITE_ROW ){
     Blob content;
     Manifest m;
-    int rid = db_column_int(&q, 0);
-    const char *zChngUuid = db_column_text(&q, 1);
-    content_get(rid, &content);
-    if( manifest_parse(&m, &content) && m.type==CFTYPE_TICKET ){
-      char *zDate = db_text(0, "SELECT datetime(%.12f)", m.rDate);
-      char zUuid[12];
-      memcpy(zUuid, zChngUuid, 10);
-      zUuid[10] = 0;
-      @
-      @ Ticket change
-      @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zUuid)</a>]</a>
+    char zShort[12];
+    const char *zDate = db_column_text(&q, 0);
+    int rid = db_column_int(&q, 1);
+    const char *zChngUuid = db_column_text(&q, 2);
+    const char *zFile = db_column_text(&q, 4);
+    memcpy(zShort, zChngUuid, 10);
+    zShort[10] = 0;
+    if( zFile!=0 ){
+      const char *zSrc = db_column_text(&q, 3);
+      const char *zUser = db_column_text(&q, 5);
+      if( zSrc==0 || zSrc[0]==0 ){
+        @
+        @ <p>Delete attachment "%h(zFile)"
+      }else{
+        @
+        @ <p>Add attachment "%h(zFile)"
+      }
+      @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zShort)</a>]
       @ (rid %d(rid)) by
-      hyperlink_to_user(m.zUser,zDate," on");
-      hyperlink_to_date(zDate, ":");
-      free(zDate);
-      ticket_output_change_artifact(&m);
+      hyperlink_to_user(zUser,zDate," on");
+      hyperlink_to_date(zDate, ".</p>");
+    }else{
+      content_get(rid, &content);
+      if( manifest_parse(&m, &content) && m.type==CFTYPE_TICKET ){
+        @
+        @ <p>Ticket change
+        @ [<a href="%s(g.zTop)/artifact/%T(zChngUuid)">%s(zShort)</a>]
+        @ (rid %d(rid)) by
+        hyperlink_to_user(m.zUser,zDate," on");
+        hyperlink_to_date(zDate, ":");
+        ticket_output_change_artifact(&m);
+        @ </p>
+      }
+      manifest_clear(&m);
     }
-    manifest_clear(&m);
   }
   db_finalize(&q);
   style_footer();
 }
 

Changes to src/wiki.c

@@ -127,10 +127,12 @@
   int isSandbox;
   Blob wiki;
   Manifest m;
   const char *zPageName;
   char *zBody = mprintf("%s","<i>Empty Page</i>");
+  Stmt q;
+  int cnt = 0;
 
   login_check_credentials();
   if( !g.okRdWiki ){ login_needed(); return; }
   zPageName = P("name");
   if( zPageName==0 ){
@@ -152,11 +154,12 @@
       @ <li>  Create a <a href="%s(g.zBaseURL)/wikinew">new wiki page</a>.</li>
     }
     @ <li> <a href="%s(g.zBaseURL)/wcontent">List of All Wiki Pages</a>
     @      available on this server.</li>
 	@ <li> <form method="GET" action="%s(g.zBaseURL)/wfind">
-	@     Search wiki titles: <input type="text" name="title"/> &nbsp; <input type="submit" />
+	@     Search wiki titles: <input type="text" name="title"/>
+        @  &nbsp; <input type="submit" />
 	@ </li>
     @ </ul>
     style_footer();
     return;
   }
@@ -186,10 +189,15 @@
   if( !g.isHome ){
     if( (rid && g.okWrWiki) || (!rid && g.okNewWiki) ){
       style_submenu_element("Edit", "Edit Wiki Page", "%s/wikiedit?name=%T",
            g.zTop, zPageName);
     }
+    if( rid && g.okWrWiki && g.okAttach ){
+      style_submenu_element("Attach", "Add An Attachment",
+           "%s/attachadd?page=%T&from=%s/wiki%%3fname=%T",
+           g.zTop, zPageName, g.zTop, zPageName);
+    }
     if( rid && g.okApndWiki ){
       style_submenu_element("Append", "Add A Comment", "%s/wikiappend?name=%T",
            g.zTop, zPageName);
     }
     if( g.okHistory ){
@@ -199,10 +207,38 @@
   }
   style_header(zPageName);
   blob_init(&wiki, zBody, -1);
   wiki_convert(&wiki, 0, 0);
   blob_reset(&wiki);
+
+  db_prepare(&q,
+     "SELECT datetime(mtime,'localtime'), filename, user"
+     "  FROM attachment"
+     " WHERE isLatest AND src!='' AND target=%Q"
+     " ORDER BY mtime DESC",
+     zPageName);
+  while( db_step(&q)==SQLITE_ROW ){
+    const char *zDate = db_column_text(&q, 0);
+    const char *zFile = db_column_text(&q, 1);
+    const char *zUser = db_column_text(&q, 2);
+    if( cnt==0 ){
+      @ <hr><h2>Attachments:</h2>
+      @ <ul>
+    }
+    cnt++;
+    @ <li><a href="%s(g.zTop)/attachview?page=%s(zPageName)&file=%t(zFile)">
+    @ %h(zFile)</a> add by %h(zUser) on
+    hyperlink_to_date(zDate, ".");
+    if( g.okWrWiki && g.okAttach ){
+      @ [<a href="%s(g.zTop)/attachdelete?page=%s(zPageName)&file=%t(zFile)&from=%s(g.zTop)/wiki%%3fname=%s(zPageName)">delete</a>]
+    }
+  }
+  if( cnt ){
+    @ </ul>
+  }
+  db_finalize(&q);
+
   if( !isSandbox ){
     manifest_clear(&m);
   }
   style_footer();
 }
@@ -515,11 +551,13 @@
 /*
 ** Function called to output extra text at the end of each line in
 ** a wiki history listing.
 */
 static void wiki_history_extra(int rid){
-  @ <a href="%s(g.zTop)/wdiff?name=%t(zWikiPageName)&a=%d(rid)">[diff]</a>
+  if( db_exists("SELECT 1 FROM tagxref WHERE rid=%d", rid) ){
+    @ <a href="%s(g.zTop)/wdiff?name=%t(zWikiPageName)&a=%d(rid)">[diff]</a>
+  }
 }
 
 /*
 ** WEBPAGE: whistory
 ** URL: /whistory?name=PAGENAME
@@ -538,13 +576,15 @@
   style_header(zTitle);
   free(zTitle);
 
   zSQL = mprintf("%s AND event.objid IN "
                  "  (SELECT rid FROM tagxref WHERE tagid="
-                       "(SELECT tagid FROM tag WHERE tagname='wiki-%q'))"
+                       "(SELECT tagid FROM tag WHERE tagname='wiki-%q')"
+                 "   UNION SELECT attachid FROM attachment"
+                          " WHERE target=%Q)"
                  "ORDER BY mtime DESC",
-                 timeline_query_for_www(), zPageName);
+                 timeline_query_for_www(), zPageName, zPageName);
   db_prepare(&q, zSQL);
   free(zSQL);
   zWikiPageName = zPageName;
   www_print_timeline(&q, TIMELINE_ARTID, wiki_history_extra);
   db_finalize(&q);
@@ -605,25 +645,42 @@
 }
 
 /*
 ** WEBPAGE: wcontent
 **
+**     all=1         Show deleted pages
+**
 ** List all available wiki pages with date created and last modified.
 */
 void wcontent_page(void){
   Stmt q;
+  int showAll = P("all")!=0;
+
   login_check_credentials();
   if( !g.okRdWiki ){ login_needed(); return; }
   style_header("Available Wiki Pages");
+  if( showAll ){
+    style_submenu_element("Active", "Only Active Pages", "%s/wcontent", g.zTop);
+  }else{
+    style_submenu_element("All", "All", "%s/wcontent?all=1", g.zTop);
+  }
   @ <ul>
   db_prepare(&q,
-    "SELECT substr(tagname, 6, 1000) FROM tag WHERE tagname GLOB 'wiki-*'"
+    "SELECT"
+    "  substr(tagname, 6),"
+    "  (SELECT value FROM tagxref WHERE tagid=tag.tagid ORDER BY mtime DESC)"
+    "  FROM tag WHERE tagname GLOB 'wiki-*'"
     " ORDER BY lower(tagname)"
   );
   while( db_step(&q)==SQLITE_ROW ){
     const char *zName = db_column_text(&q, 0);
-    @ <li><a href="%s(g.zBaseURL)/wiki?name=%T(zName)">%h(zName)</a></li>
+    int size = db_column_int(&q, 1);
+    if( size>0 ){
+      @ <li><a href="%s(g.zTop)/wiki?name=%T(zName)">%h(zName)</a></li>
+    }else if( showAll ){
+      @ <li><a href="%s(g.zTop)/wiki?name=%T(zName)"><s>%h(zName)</s></a></li>
+    }
   }
   db_finalize(&q);
   @ </ul>
   style_footer();
 }