1 /*
2 ** Copyright (c) 2008 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 **   drh@hwaci.com
14 **   http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** This file contains code to implement the file browser web interface.
19 */
20 #include "config.h"
21 #include "browse.h"
22 #include <assert.h>
23 
24 /*
25 ** This is the implementation of the "pathelement(X,N)" SQL function.
26 **
27 ** If X is a unix-like pathname (with "/" separators) and N is an
28 ** integer, then skip the initial N characters of X and return the
29 ** name of the path component that begins on the N+1th character
30 ** (numbered from 0).  If the path component is a directory (if
31 ** it is followed by other path components) then prepend "/".
32 **
33 ** Examples:
34 **
35 **      pathelement('abc/pqr/xyz', 4)  ->  '/pqr'
36 **      pathelement('abc/pqr', 4)      ->  'pqr'
37 **      pathelement('abc/pqr/xyz', 0)  ->  '/abc'
38 */
pathelementFunc(sqlite3_context * context,int argc,sqlite3_value ** argv)39 void pathelementFunc(
40   sqlite3_context *context,
41   int argc,
42   sqlite3_value **argv
43 ){
44   const unsigned char *z;
45   int len, n, i;
46   char *zOut;
47 
48   assert( argc==2 );
49   z = sqlite3_value_text(argv[0]);
50   if( z==0 ) return;
51   len = sqlite3_value_bytes(argv[0]);
52   n = sqlite3_value_int(argv[1]);
53   if( len<=n ) return;
54   if( n>0 && z[n-1]!='/' ) return;
55   for(i=n; i<len && z[i]!='/'; i++){}
56   if( i==len ){
57     sqlite3_result_text(context, (char*)&z[n], len-n, SQLITE_TRANSIENT);
58   }else{
59     zOut = sqlite3_mprintf("/%.*s", i-n, &z[n]);
60     sqlite3_result_text(context, zOut, i-n+1, sqlite3_free);
61   }
62 }
63 
64 /*
65 ** Flag arguments for hyperlinked_path()
66 */
67 #if INTERFACE
68 # define LINKPATH_FINFO  0x0001       /* Link final term to /finfo */
69 # define LINKPATH_FILE   0x0002       /* Link final term to /file */
70 #endif
71 
72 /*
73 ** Given a pathname which is a relative path from the root of
74 ** the repository to a file or directory, compute a string which
75 ** is an HTML rendering of that path with hyperlinks on each
76 ** directory component of the path where the hyperlink redirects
77 ** to the "dir" page for the directory.
78 **
79 ** There is no hyperlink on the file element of the path.
80 **
81 ** The computed string is appended to the pOut blob.  pOut should
82 ** have already been initialized.
83 */
hyperlinked_path(const char * zPath,Blob * pOut,const char * zCI,const char * zURI,const char * zREx,unsigned int mFlags)84 void hyperlinked_path(
85   const char *zPath,    /* Path to render */
86   Blob *pOut,           /* Write into this blob */
87   const char *zCI,      /* check-in name, or NULL */
88   const char *zURI,     /* "dir" or "tree" */
89   const char *zREx,     /* Extra query parameters */
90   unsigned int mFlags   /* Extra flags */
91 ){
92   int i, j;
93   char *zSep = "";
94 
95   for(i=0; zPath[i]; i=j){
96     for(j=i; zPath[j] && zPath[j]!='/'; j++){}
97     if( zPath[j]==0 ){
98       if( mFlags & LINKPATH_FILE ){
99         zURI = "file";
100       }else if( mFlags & LINKPATH_FINFO ){
101         zURI = "finfo";
102       }else{
103         blob_appendf(pOut, "/%h", zPath+i);
104         break;
105       }
106     }
107     if( zCI ){
108       char *zLink = href("%R/%s?name=%#T%s&ci=%T", zURI, j, zPath, zREx,zCI);
109       blob_appendf(pOut, "%s%z%#h</a>",
110                    zSep, zLink, j-i, &zPath[i]);
111     }else{
112       char *zLink = href("%R/%s?name=%#T%s", zURI, j, zPath, zREx);
113       blob_appendf(pOut, "%s%z%#h</a>",
114                    zSep, zLink, j-i, &zPath[i]);
115     }
116     zSep = "/";
117     while( zPath[j]=='/' ){ j++; }
118   }
119 }
120 
121 
122 /*
123 ** WEBPAGE: dir
124 **
125 ** Show the files and subdirectories within a single directory of the
126 ** source tree.  Only files for a single check-in are shown if the ci=
127 ** query parameter is present.  If ci= is missing, the union of files
128 ** across all check-ins is shown.
129 **
130 ** Query parameters:
131 **
132 **    ci=LABEL         Show only files in this check-in.  Optional.
133 **    name=PATH        Directory to display.  Optional.  Top-level if missing
134 **    re=REGEXP        Show only files matching REGEXP
135 **    type=TYPE        TYPE=flat: use this display
136 **                     TYPE=tree: use the /tree display instead
137 **    noreadme         Do not attempt to display the README file.
138 */
page_dir(void)139 void page_dir(void){
140   char *zD = fossil_strdup(P("name"));
141   int nD = zD ? strlen(zD)+1 : 0;
142   int mxLen;
143   char *zPrefix;
144   Stmt q;
145   const char *zCI = P("ci");
146   int rid = 0;
147   char *zUuid = 0;
148   Manifest *pM = 0;
149   const char *zSubdirLink;
150   int linkTrunk = 1;
151   int linkTip = 1;
152   HQuery sURI;
153   int isSymbolicCI = 0;   /* ci= is symbolic name, not a hash prefix */
154   int isBranchCI = 0;     /* True if ci= refers to a branch name */
155   char *zHeader = 0;
156   const char *zRegexp;    /* The re= query parameter */
157   char *zMatch;           /* Extra title text describing the match */
158 
159   if( zCI && strlen(zCI)==0 ){ zCI = 0; }
160   if( strcmp(PD("type","flat"),"tree")==0 ){ page_tree(); return; }
161   login_check_credentials();
162   if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
163   while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
164 
165   /* If the name= parameter is an empty string, make it a NULL pointer */
166   if( zD && strlen(zD)==0 ){ zD = 0; }
167 
168   /* If a specific check-in is requested, fetch and parse it.  If the
169   ** specific check-in does not exist, clear zCI.  zCI==0 will cause all
170   ** files from all check-ins to be displayed.
171   */
172   if( zCI ){
173     pM = manifest_get_by_name(zCI, &rid);
174     if( pM ){
175       int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
176       linkTrunk = trunkRid && rid != trunkRid;
177       linkTip = rid != symbolic_name_to_rid("tip", "ci");
178       zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
179       isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI))!=0);
180       isBranchCI = branch_includes_uuid(zCI, zUuid);
181       Th_Store("current_checkin", zCI);
182     }else{
183       zCI = 0;
184     }
185   }
186 
187   assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
188   if( zD==0 ){
189     if( zCI ){
190       zHeader = mprintf("Top-level Files of %s", zCI);
191     }else{
192       zHeader = mprintf("All Top-level Files");
193     }
194   }else{
195     if( zCI ){
196       zHeader = mprintf("Files in %s/ of %s", zD, zCI);
197     }else{
198       zHeader = mprintf("All File in %s/", zD);
199     }
200   }
201   zRegexp = P("re");
202   if( zRegexp ){
203     zHeader = mprintf("%z matching \"%s\"", zHeader, zRegexp);
204     zMatch = mprintf(" matching \"%h\"", zRegexp);
205   }else{
206     zMatch = "";
207   }
208   style_header("%s", zHeader);
209   fossil_free(zHeader);
210   style_adunit_config(ADUNIT_RIGHT_OK);
211   sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
212                           pathelementFunc, 0, 0);
213   url_initialize(&sURI, "dir");
214   cgi_query_parameters_to_url(&sURI);
215 
216   /* Compute the title of the page */
217   if( zD ){
218     Blob dirname;
219     blob_init(&dirname, 0, 0);
220     hyperlinked_path(zD, &dirname, zCI, "dir", "", 0);
221     @ <h2>Files in directory %s(blob_str(&dirname)) \
222     blob_reset(&dirname);
223     zPrefix = mprintf("%s/", zD);
224     style_submenu_element("Top-Level", "%s",
225                           url_render(&sURI, "name", 0, 0, 0));
226   }else{
227     @ <h2>Files in the top-level directory \
228     zPrefix = "";
229   }
230   if( zCI ){
231     if( fossil_strcmp(zCI,"tip")==0 ){
232       @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a>\
233       @ %s(zMatch)</h2>
234     }else if( isBranchCI ){
235       @ from the %z(href("%R/info?name=%T",zCI))latest check-in</a> \
236       @ of branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a>\
237       @ %s(zMatch)</h2>
238     }else {
239       @ of check-in %z(href("%R/info?name=%T",zCI))%h(zCI)</a>\
240       @ %s(zMatch)</h2>
241     }
242     zSubdirLink = mprintf("%R/dir?ci=%T&name=%T", zCI, zPrefix);
243     if( nD==0 ){
244       style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
245     }
246   }else{
247     @ in any check-in</h2>
248     zSubdirLink = mprintf("%R/dir?name=%T", zPrefix);
249   }
250   if( linkTrunk ){
251     style_submenu_element("Trunk", "%s",
252                           url_render(&sURI, "ci", "trunk", 0, 0));
253   }
254   if( linkTip ){
255     style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
256   }
257   if( zD ){
258     style_submenu_element("History","%R/timeline?chng=%T/*", zD);
259   }
260   style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
261   style_submenu_element("Tree-View", "%s",
262                         url_render(&sURI, "type", "tree", 0, 0));
263 
264   /* Compute the temporary table "localfiles" containing the names
265   ** of all files and subdirectories in the zD[] directory.
266   **
267   ** Subdirectory names begin with "/".  This causes them to sort
268   ** first and it also gives us an easy way to distinguish files
269   ** from directories in the loop that follows.
270   */
271   db_multi_exec(
272      "CREATE TEMP TABLE localfiles(x UNIQUE NOT NULL, u);"
273   );
274   if( zCI ){
275     Stmt ins;
276     ManifestFile *pFile;
277     ManifestFile *pPrev = 0;
278     int nPrev = 0;
279     int c;
280 
281     db_prepare(&ins,
282        "INSERT OR IGNORE INTO localfiles VALUES(pathelement(:x,0), :u)"
283     );
284     manifest_file_rewind(pM);
285     while( (pFile = manifest_file_next(pM,0))!=0 ){
286       if( nD>0
287        && (fossil_strncmp(pFile->zName, zD, nD-1)!=0
288            || pFile->zName[nD-1]!='/')
289       ){
290         continue;
291       }
292       if( pPrev
293        && fossil_strncmp(&pFile->zName[nD],&pPrev->zName[nD],nPrev)==0
294        && (pFile->zName[nD+nPrev]==0 || pFile->zName[nD+nPrev]=='/')
295       ){
296         continue;
297       }
298       db_bind_text(&ins, ":x", &pFile->zName[nD]);
299       db_bind_text(&ins, ":u", pFile->zUuid);
300       db_step(&ins);
301       db_reset(&ins);
302       pPrev = pFile;
303       for(nPrev=0; (c=pPrev->zName[nD+nPrev]) && c!='/'; nPrev++){}
304       if( c=='/' ) nPrev++;
305     }
306     db_finalize(&ins);
307   }else if( zD ){
308     db_multi_exec(
309       "INSERT OR IGNORE INTO localfiles"
310       " SELECT pathelement(name,%d), NULL FROM filename"
311       "  WHERE name GLOB '%q/*'",
312       nD, zD
313     );
314   }else{
315     db_multi_exec(
316       "INSERT OR IGNORE INTO localfiles"
317       " SELECT pathelement(name,0), NULL FROM filename"
318     );
319   }
320 
321   /* If the re=REGEXP query parameter is present, filter out names that
322   ** do not match the pattern */
323   if( zRegexp ){
324     db_multi_exec(
325       "DELETE FROM localfiles WHERE x NOT REGEXP %Q", zRegexp
326     );
327   }
328 
329   /* Generate a multi-column table listing the contents of zD[]
330   ** directory.
331   */
332   mxLen = db_int(12, "SELECT max(length(x)) FROM localfiles /*scan*/");
333   if( mxLen<12 ) mxLen = 12;
334   mxLen += (mxLen+9)/10;
335   db_prepare(&q, "SELECT x, u FROM localfiles ORDER BY x /*scan*/");
336   @ <div class="columns files" style="columns: %d(mxLen)ex auto">
337   @ <ul class="browser">
338   while( db_step(&q)==SQLITE_ROW ){
339     const char *zFN;
340     zFN = db_column_text(&q, 0);
341     if( zFN[0]=='/' ){
342       zFN++;
343       @ <li class="dir">%z(href("%s%T",zSubdirLink,zFN))%h(zFN)</a></li>
344     }else{
345       const char *zLink;
346       if( zCI ){
347         zLink = href("%R/file?name=%T%T&ci=%T",zPrefix,zFN,zCI);
348       }else{
349         zLink = href("%R/finfo?name=%T%T",zPrefix,zFN);
350       }
351       @ <li class="%z(fileext_class(zFN))">%z(zLink)%h(zFN)</a></li>
352     }
353   }
354   db_finalize(&q);
355   manifest_destroy(pM);
356   @ </ul></div>
357 
358   /* If the "noreadme" query parameter is present, do not try to
359   ** show the content of the README file.
360   */
361   if( P("noreadme")!=0 ){
362     style_finish_page();
363     return;
364   }
365 
366   /* If the directory contains a readme file, then display its content below
367   ** the list of files
368   */
369   db_prepare(&q,
370     "SELECT x, u FROM localfiles"
371     " WHERE x COLLATE nocase IN"
372     " ('readme','readme.txt','readme.md','readme.wiki','readme.markdown',"
373     " 'readme.html') ORDER BY x LIMIT 1;"
374   );
375   if( db_step(&q)==SQLITE_ROW ){
376     const char *zName = db_column_text(&q,0);
377     const char *zUuid = db_column_text(&q,1);
378     if( zUuid ){
379       rid = fast_uuid_to_rid(zUuid);
380     }else{
381       if( zD ){
382         rid = db_int(0,
383            "SELECT fid FROM filename, mlink, event"
384            " WHERE name='%q/%q'"
385            "   AND mlink.fnid=filename.fnid"
386            "   AND event.objid=mlink.mid"
387            " ORDER BY event.mtime DESC LIMIT 1",
388            zD, zName
389         );
390       }else{
391         rid = db_int(0,
392            "SELECT fid FROM filename, mlink, event"
393            " WHERE name='%q'"
394            "   AND mlink.fnid=filename.fnid"
395            "   AND event.objid=mlink.mid"
396            " ORDER BY event.mtime DESC LIMIT 1",
397            zName
398         );
399       }
400     }
401     if( rid ){
402       @ <hr>
403       if( sqlite3_strlike("readme.html", zName, 0)==0 ){
404         if( zUuid==0 ){
405           zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
406         }
407         @ <iframe src="%R/raw/%s(zUuid)"
408         @ width="100%%" frameborder="0" marginwidth="0" marginheight="0"
409         @ sandbox="allow-same-origin"
410         @ onload="this.height=this.contentDocument.documentElement.scrollHeight;">
411         @ </iframe>
412       }else{
413         Blob content;
414         const char *zMime = mimetype_from_name(zName);
415         content_get(rid, &content);
416         safe_html_context(DOCSRC_FILE);
417         wiki_render_by_mimetype(&content, zMime);
418         document_emit_js();
419       }
420     }
421   }
422   db_finalize(&q);
423   style_finish_page();
424 }
425 
426 /*
427 ** Objects used by the "tree" webpage.
428 */
429 typedef struct FileTreeNode FileTreeNode;
430 typedef struct FileTree FileTree;
431 
432 /*
433 ** A single line of the file hierarchy
434 */
435 struct FileTreeNode {
436   FileTreeNode *pNext;      /* Next entry in an ordered list of them all */
437   FileTreeNode *pParent;    /* Directory containing this entry */
438   FileTreeNode *pSibling;   /* Next element in the same subdirectory */
439   FileTreeNode *pChild;     /* List of child nodes */
440   FileTreeNode *pLastChild; /* Last child on the pChild list */
441   char *zName;              /* Name of this entry.  The "tail" */
442   char *zFullName;          /* Full pathname of this entry */
443   char *zUuid;              /* Artifact hash of this file.  May be NULL. */
444   double mtime;             /* Modification time for this entry */
445   unsigned nFullName;       /* Length of zFullName */
446   unsigned iLevel;          /* Levels of parent directories */
447 };
448 
449 /*
450 ** A complete file hierarchy
451 */
452 struct FileTree {
453   FileTreeNode *pFirst;     /* First line of the list */
454   FileTreeNode *pLast;      /* Last line of the list */
455   FileTreeNode *pLastTop;   /* Last top-level node */
456 };
457 
458 /*
459 ** Add one or more new FileTreeNodes to the FileTree object so that the
460 ** leaf object zPathname is at the end of the node list.
461 **
462 ** The caller invokes this routine once for each leaf node (each file
463 ** as opposed to each directory).  This routine fills in any missing
464 ** intermediate nodes automatically.
465 **
466 ** When constructing a list of FileTreeNodes, all entries that have
467 ** a common directory prefix must be added consecutively in order for
468 ** the tree to be constructed properly.
469 */
tree_add_node(FileTree * pTree,const char * zPath,const char * zUuid,double mtime)470 static void tree_add_node(
471   FileTree *pTree,         /* Tree into which nodes are added */
472   const char *zPath,       /* The full pathname of file to add */
473   const char *zUuid,       /* Hash of the file.  Might be NULL. */
474   double mtime             /* Modification time for this entry */
475 ){
476   int i;
477   FileTreeNode *pParent;   /* Parent (directory) of the next node to insert */
478 
479   /* Make pParent point to the most recent ancestor of zPath, or
480   ** NULL if there are no prior entires that are a container for zPath.
481   */
482   pParent = pTree->pLast;
483   while( pParent!=0 &&
484       ( strncmp(pParent->zFullName, zPath, pParent->nFullName)!=0
485         || zPath[pParent->nFullName]!='/' )
486   ){
487     pParent = pParent->pParent;
488   }
489   i = pParent ? pParent->nFullName+1 : 0;
490   while( zPath[i] ){
491     FileTreeNode *pNew;
492     int iStart = i;
493     int nByte;
494     while( zPath[i] && zPath[i]!='/' ){ i++; }
495     nByte = sizeof(*pNew) + i + 1;
496     if( zUuid!=0 && zPath[i]==0 ) nByte += HNAME_MAX+1;
497     pNew = fossil_malloc( nByte );
498     memset(pNew, 0, sizeof(*pNew));
499     pNew->zFullName = (char*)&pNew[1];
500     memcpy(pNew->zFullName, zPath, i);
501     pNew->zFullName[i] = 0;
502     pNew->nFullName = i;
503     if( zUuid!=0 && zPath[i]==0 ){
504       pNew->zUuid = pNew->zFullName + i + 1;
505       memcpy(pNew->zUuid, zUuid, strlen(zUuid)+1);
506     }
507     pNew->zName = pNew->zFullName + iStart;
508     if( pTree->pLast ){
509       pTree->pLast->pNext = pNew;
510     }else{
511       pTree->pFirst = pNew;
512     }
513     pTree->pLast = pNew;
514     pNew->pParent = pParent;
515     if( pParent ){
516       if( pParent->pChild ){
517         pParent->pLastChild->pSibling = pNew;
518       }else{
519         pParent->pChild = pNew;
520       }
521       pNew->iLevel = pParent->iLevel + 1;
522       pParent->pLastChild = pNew;
523     }else{
524       if( pTree->pLastTop ) pTree->pLastTop->pSibling = pNew;
525       pTree->pLastTop = pNew;
526     }
527     pNew->mtime = mtime;
528     while( zPath[i]=='/' ){ i++; }
529     pParent = pNew;
530   }
531   while( pParent && pParent->pParent ){
532     if( pParent->pParent->mtime < pParent->mtime ){
533       pParent->pParent->mtime = pParent->mtime;
534     }
535     pParent = pParent->pParent;
536   }
537 }
538 
539 /* Comparison function for two FileTreeNode objects.  Sort first by
540 ** mtime (larger numbers first) and then by zName (smaller names first).
541 **
542 ** Return negative if pLeft<pRight.
543 ** Return positive if pLeft>pRight.
544 ** Return zero if pLeft==pRight.
545 */
compareNodes(FileTreeNode * pLeft,FileTreeNode * pRight)546 static int compareNodes(FileTreeNode *pLeft, FileTreeNode *pRight){
547   if( pLeft->mtime>pRight->mtime ) return -1;
548   if( pLeft->mtime<pRight->mtime ) return +1;
549   return fossil_stricmp(pLeft->zName, pRight->zName);
550 }
551 
552 /* Merge together two sorted lists of FileTreeNode objects */
mergeNodes(FileTreeNode * pLeft,FileTreeNode * pRight)553 static FileTreeNode *mergeNodes(FileTreeNode *pLeft,  FileTreeNode *pRight){
554   FileTreeNode *pEnd;
555   FileTreeNode base;
556   pEnd = &base;
557   while( pLeft && pRight ){
558     if( compareNodes(pLeft,pRight)<=0 ){
559       pEnd = pEnd->pSibling = pLeft;
560       pLeft = pLeft->pSibling;
561     }else{
562       pEnd = pEnd->pSibling = pRight;
563       pRight = pRight->pSibling;
564     }
565   }
566   if( pLeft ){
567     pEnd->pSibling = pLeft;
568   }else{
569     pEnd->pSibling = pRight;
570   }
571   return base.pSibling;
572 }
573 
574 /* Sort a list of FileTreeNode objects in mtime order. */
sortNodesByMtime(FileTreeNode * p)575 static FileTreeNode *sortNodesByMtime(FileTreeNode *p){
576   FileTreeNode *a[30];
577   FileTreeNode *pX;
578   int i;
579 
580   memset(a, 0, sizeof(a));
581   while( p ){
582     pX = p;
583     p = pX->pSibling;
584     pX->pSibling = 0;
585     for(i=0; i<count(a)-1 && a[i]!=0; i++){
586       pX = mergeNodes(a[i], pX);
587       a[i] = 0;
588     }
589     a[i] = mergeNodes(a[i], pX);
590   }
591   pX = 0;
592   for(i=0; i<count(a); i++){
593     pX = mergeNodes(a[i], pX);
594   }
595   return pX;
596 }
597 
598 /* Sort an entire FileTreeNode tree by mtime
599 **
600 ** This routine invalidates the following fields:
601 **
602 **     FileTreeNode.pLastChild
603 **     FileTreeNode.pNext
604 **
605 ** Use relinkTree to reconnect the pNext pointers.
606 */
sortTreeByMtime(FileTreeNode * p)607 static FileTreeNode *sortTreeByMtime(FileTreeNode *p){
608   FileTreeNode *pX;
609   for(pX=p; pX; pX=pX->pSibling){
610     if( pX->pChild ) pX->pChild = sortTreeByMtime(pX->pChild);
611   }
612   return sortNodesByMtime(p);
613 }
614 
615 /* Reconstruct the FileTree by reconnecting the FileTreeNode.pNext
616 ** fields in sequential order.
617 */
relinkTree(FileTree * pTree,FileTreeNode * pRoot)618 static void relinkTree(FileTree *pTree, FileTreeNode *pRoot){
619   while( pRoot ){
620     if( pTree->pLast ){
621       pTree->pLast->pNext = pRoot;
622     }else{
623       pTree->pFirst = pRoot;
624     }
625     pTree->pLast = pRoot;
626     if( pRoot->pChild ) relinkTree(pTree, pRoot->pChild);
627     pRoot = pRoot->pSibling;
628   }
629   if( pTree->pLast ) pTree->pLast->pNext = 0;
630 }
631 
632 
633 /*
634 ** WEBPAGE: tree
635 **
636 ** Show the files using a tree-view.  If the ci= query parameter is present
637 ** then show only the files for the check-in identified.  If ci= is omitted,
638 ** then show the union of files over all check-ins.
639 **
640 ** The type=tree query parameter is required or else the /dir format is
641 ** used.
642 **
643 ** Query parameters:
644 **
645 **    type=tree        Required to prevent use of /dir format
646 **    name=PATH        Directory to display.  Optional
647 **    ci=LABEL         Show only files in this check-in.  Optional.
648 **    re=REGEXP        Show only files matching REGEXP.  Optional.
649 **    expand           Begin with the tree fully expanded.
650 **    nofiles          Show directories (folders) only.  Omit files.
651 **    mtime            Order directory elements by decreasing mtime
652 */
page_tree(void)653 void page_tree(void){
654   char *zD = fossil_strdup(P("name"));
655   int nD = zD ? strlen(zD)+1 : 0;
656   const char *zCI = P("ci");
657   int rid = 0;
658   char *zUuid = 0;
659   Blob dirname;
660   Manifest *pM = 0;
661   double rNow = 0;
662   char *zNow = 0;
663   int useMtime = atoi(PD("mtime","0"));
664   int nFile = 0;           /* Number of files (or folders with "nofiles") */
665   int linkTrunk = 1;       /* include link to "trunk" */
666   int linkTip = 1;         /* include link to "tip" */
667   const char *zRE;         /* the value for the re=REGEXP query parameter */
668   const char *zObjType;    /* "files" by default or "folders" for "nofiles" */
669   char *zREx = "";         /* Extra parameters for path hyperlinks */
670   ReCompiled *pRE = 0;     /* Compiled regular expression */
671   FileTreeNode *p;         /* One line of the tree */
672   FileTree sTree;          /* The complete tree of files */
673   HQuery sURI;             /* Hyperlink */
674   int startExpanded;       /* True to start out with the tree expanded */
675   int showDirOnly;         /* Show directories only.  Omit files */
676   int nDir = 0;            /* Number of directories. Used for ID attributes */
677   char *zProjectName = db_get("project-name", 0);
678   int isSymbolicCI = 0;    /* ci= is a symbolic name, not a hash prefix */
679   int isBranchCI = 0;      /* ci= refers to a branch name */
680   char *zHeader = 0;
681 
682   if( zCI && strlen(zCI)==0 ){ zCI = 0; }
683   if( strcmp(PD("type","flat"),"flat")==0 ){ page_dir(); return; }
684   memset(&sTree, 0, sizeof(sTree));
685   login_check_credentials();
686   if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
687   while( nD>1 && zD[nD-2]=='/' ){ zD[(--nD)-1] = 0; }
688   sqlite3_create_function(g.db, "pathelement", 2, SQLITE_UTF8, 0,
689                           pathelementFunc, 0, 0);
690   url_initialize(&sURI, "tree");
691   cgi_query_parameters_to_url(&sURI);
692   if( PB("nofiles") ){
693     showDirOnly = 1;
694   }else{
695     showDirOnly = 0;
696   }
697   style_adunit_config(ADUNIT_RIGHT_OK);
698   if( PB("expand") ){
699     startExpanded = 1;
700   }else{
701     startExpanded = 0;
702   }
703 
704   /* If a regular expression is specified, compile it */
705   zRE = P("re");
706   if( zRE ){
707     re_compile(&pRE, zRE, 0);
708     zREx = mprintf("&re=%T", zRE);
709   }
710 
711   /* If the name= parameter is an empty string, make it a NULL pointer */
712   if( zD && strlen(zD)==0 ){ zD = 0; }
713 
714   /* If a specific check-in is requested, fetch and parse it.  If the
715   ** specific check-in does not exist, clear zCI.  zCI==0 will cause all
716   ** files from all check-ins to be displayed.
717   */
718   if( zCI ){
719     pM = manifest_get_by_name(zCI, &rid);
720     if( pM ){
721       int trunkRid = symbolic_name_to_rid("tag:trunk", "ci");
722       linkTrunk = trunkRid && rid != trunkRid;
723       linkTip = rid != symbolic_name_to_rid("tip", "ci");
724       zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", rid);
725       rNow = db_double(0.0, "SELECT mtime FROM event WHERE objid=%d", rid);
726       zNow = db_text("", "SELECT datetime(mtime,toLocal())"
727                          " FROM event WHERE objid=%d", rid);
728       isSymbolicCI = (sqlite3_strnicmp(zUuid, zCI, strlen(zCI)) != 0);
729       isBranchCI = branch_includes_uuid(zCI, zUuid);
730       Th_Store("current_checkin", zCI);
731     }else{
732       zCI = 0;
733     }
734   }
735   if( zCI==0 ){
736     rNow = db_double(0.0, "SELECT max(mtime) FROM event");
737     zNow = db_text("", "SELECT datetime(max(mtime),toLocal()) FROM event");
738   }
739 
740   assert( isSymbolicCI==0 || (zCI!=0 && zCI[0]!=0) );
741   if( zD==0 ){
742     if( zCI ){
743       zHeader = mprintf("Top-level Files of %s", zCI);
744     }else{
745       zHeader = mprintf("All Top-level Files");
746     }
747   }else{
748     if( zCI ){
749       zHeader = mprintf("Files in %s/ of %s", zD, zCI);
750     }else{
751       zHeader = mprintf("All File in %s/", zD);
752     }
753   }
754   style_header("%s", zHeader);
755   fossil_free(zHeader);
756 
757   /* Compute the title of the page */
758   blob_zero(&dirname);
759   if( zD ){
760     blob_append(&dirname, "within directory ", -1);
761     hyperlinked_path(zD, &dirname, zCI, "tree", zREx, 0);
762     if( zRE ) blob_appendf(&dirname, " matching \"%s\"", zRE);
763     style_submenu_element("Top-Level", "%s",
764                           url_render(&sURI, "name", 0, 0, 0));
765   }else if( zRE ){
766     blob_appendf(&dirname, "matching \"%s\"", zRE);
767   }
768   style_submenu_binary("mtime","Sort By Time","Sort By Filename", 0);
769   if( zCI ){
770     style_submenu_element("All", "%s", url_render(&sURI, "ci", 0, 0, 0));
771     if( nD==0 && !showDirOnly ){
772       style_submenu_element("File Ages", "%R/fileage?name=%T", zCI);
773     }
774   }
775   if( linkTrunk ){
776     style_submenu_element("Trunk", "%s",
777                           url_render(&sURI, "ci", "trunk", 0, 0));
778   }
779   if( linkTip ){
780     style_submenu_element("Tip", "%s", url_render(&sURI, "ci", "tip", 0, 0));
781   }
782   style_submenu_element("Flat-View", "%s",
783                         url_render(&sURI, "type", "flat", 0, 0));
784 
785   /* Compute the file hierarchy.
786   */
787   if( zCI ){
788     Stmt q;
789     compute_fileage(rid, 0);
790     db_prepare(&q,
791        "SELECT filename.name, blob.uuid, fileage.mtime\n"
792        "  FROM fileage, filename, blob\n"
793        " WHERE filename.fnid=fileage.fnid\n"
794        "   AND blob.rid=fileage.fid\n"
795        " ORDER BY filename.name COLLATE nocase;"
796     );
797     while( db_step(&q)==SQLITE_ROW ){
798       const char *zFile = db_column_text(&q,0);
799       const char *zUuid = db_column_text(&q,1);
800       double mtime = db_column_double(&q,2);
801       if( nD>0 && (fossil_strncmp(zFile, zD, nD-1)!=0 || zFile[nD-1]!='/') ){
802         continue;
803       }
804       if( pRE && re_match(pRE, (const unsigned char*)zFile, -1)==0 ) continue;
805       tree_add_node(&sTree, zFile, zUuid, mtime);
806       nFile++;
807     }
808     db_finalize(&q);
809   }else{
810     Stmt q;
811     db_prepare(&q,
812       "SELECT\n"
813       "    (SELECT name FROM filename WHERE filename.fnid=mlink.fnid),\n"
814       "    (SELECT uuid FROM blob WHERE blob.rid=mlink.fid),\n"
815       "    max(event.mtime)\n"
816       "  FROM mlink JOIN event ON event.objid=mlink.mid\n"
817       " GROUP BY mlink.fnid\n"
818       " ORDER BY 1 COLLATE nocase;");
819     while( db_step(&q)==SQLITE_ROW ){
820       const char *zName = db_column_text(&q, 0);
821       const char *zUuid = db_column_text(&q,1);
822       double mtime = db_column_double(&q,2);
823       if( nD>0 && (fossil_strncmp(zName, zD, nD-1)!=0 || zName[nD-1]!='/') ){
824         continue;
825       }
826       if( pRE && re_match(pRE, (const u8*)zName, -1)==0 ) continue;
827       tree_add_node(&sTree, zName, zUuid, mtime);
828       nFile++;
829     }
830     db_finalize(&q);
831   }
832   style_submenu_checkbox("nofiles", "Folders Only", 0, 0);
833 
834   if( showDirOnly ){
835     for(nFile=0, p=sTree.pFirst; p; p=p->pNext){
836       if( p->pChild!=0 && p->nFullName>nD ) nFile++;
837     }
838     zObjType = "Folders";
839   }else{
840     zObjType = "Files";
841   }
842 
843   if( zCI && strcmp(zCI,"tip")==0 ){
844     @ <h2>%s(zObjType) in the %z(href("%R/info?name=tip"))latest check-in</a>
845   }else if( isBranchCI ){
846     @ <h2>%s(zObjType) in the %z(href("%R/info?name=%T",zCI))latest check-in\
847     @ </a> for branch %z(href("%R/timeline?r=%T",zCI))%h(zCI)</a>
848     if( blob_size(&dirname) ){
849       @ and %s(blob_str(&dirname))</h2>
850     }
851   }else if( zCI ){
852     @ <h2>%s(zObjType) for check-in \
853     @ %z(href("%R/info?name=%T",zCI))%h(zCI)</a></h2>
854     if( blob_size(&dirname) ){
855       @ and %s(blob_str(&dirname))</h2>
856     }
857   }else{
858     int n = db_int(0, "SELECT count(*) FROM plink");
859     @ <h2>%s(zObjType) from all %d(n) check-ins %s(blob_str(&dirname))
860   }
861   if( useMtime ){
862     @ sorted by modification time</h2>
863   }else{
864     @ sorted by filename</h2>
865   }
866 
867 
868   /* Generate tree of lists.
869   **
870   ** Each file and directory is a list element: <li>.  Files have class=file
871   ** and if the filename as the suffix "xyz" the file also has class=file-xyz.
872   ** Directories have class=dir.  The directory specfied by the name= query
873   ** parameter (or the top-level directory if there is no name= query parameter)
874   ** adds class=subdir.
875   **
876   ** The <li> element for directories also contains a sublist <ul>
877   ** for the contents of that directory.
878   */
879   @ <div class="filetree"><ul>
880   if( nD ){
881     @ <li class="dir last">
882   }else{
883     @ <li class="dir subdir last">
884   }
885   @ <div class="filetreeline">
886   @ %z(href("%s",url_render(&sURI,"name",0,0,0)))%h(zProjectName)</a>
887   if( zNow ){
888     @ <div class="filetreeage">%s(zNow)</div>
889   }
890   @ </div>
891   @ <ul>
892   if( useMtime ){
893     p = sortTreeByMtime(sTree.pFirst);
894     memset(&sTree, 0, sizeof(sTree));
895     relinkTree(&sTree, p);
896   }
897   for(p=sTree.pFirst, nDir=0; p; p=p->pNext){
898     const char *zLastClass = p->pSibling==0 ? " last" : "";
899     if( p->pChild ){
900       const char *zSubdirClass = p->nFullName==nD-1 ? " subdir" : "";
901       @ <li class="dir%s(zSubdirClass)%s(zLastClass)"><div class="filetreeline">
902       @ %z(href("%s",url_render(&sURI,"name",p->zFullName,0,0)))%h(p->zName)</a>
903       if( p->mtime>0.0 ){
904         char *zAge = human_readable_age(rNow - p->mtime);
905         @ <div class="filetreeage">%s(zAge)</div>
906       }
907       @ </div>
908       if( startExpanded || p->nFullName<=nD ){
909         @ <ul id="dir%d(nDir)">
910       }else{
911         @ <ul id="dir%d(nDir)" class="collapsed">
912       }
913       nDir++;
914     }else if( !showDirOnly ){
915       const char *zFileClass = fileext_class(p->zName);
916       char *zLink;
917       if( zCI ){
918         zLink = href("%R/file?name=%T&ci=%T",p->zFullName,zCI);
919       }else{
920         zLink = href("%R/finfo?name=%T",p->zFullName);
921       }
922       @ <li class="%z(zFileClass)%s(zLastClass)"><div class="filetreeline">
923       @ %z(zLink)%h(p->zName)</a>
924       if( p->mtime>0 ){
925         char *zAge = human_readable_age(rNow - p->mtime);
926         @ <div class="filetreeage">%s(zAge)</div>
927       }
928       @ </div>
929     }
930     if( p->pSibling==0 ){
931       int nClose = p->iLevel - (p->pNext ? p->pNext->iLevel : 0);
932       while( nClose-- > 0 ){
933         @ </ul>
934       }
935     }
936   }
937   @ </ul>
938   @ </ul></div>
939   builtin_request_js("tree.js");
940   style_finish_page();
941 
942   /* We could free memory used by sTree here if we needed to.  But
943   ** the process is about to exit, so doing so would not really accomplish
944   ** anything useful. */
945 }
946 
947 /*
948 ** Return a CSS class name based on the given filename's extension.
949 ** Result must be freed by the caller.
950 **/
fileext_class(const char * zFilename)951 const char *fileext_class(const char *zFilename){
952   char *zClass;
953   const char *zExt = strrchr(zFilename, '.');
954   int isExt = zExt && zExt!=zFilename && zExt[1];
955   int i;
956   for( i=1; isExt && zExt[i]; i++ ) isExt &= fossil_isalnum(zExt[i]);
957   if( isExt ){
958     zClass = mprintf("file file-%s", zExt+1);
959     for( i=5; zClass[i]; i++ ) zClass[i] = fossil_tolower(zClass[i]);
960   }else{
961     zClass = mprintf("file");
962   }
963   return zClass;
964 }
965 
966 /*
967 ** SQL used to compute the age of all files in check-in :ckin whose
968 ** names match :glob
969 */
970 static const char zComputeFileAgeSetup[] =
971 @ CREATE TABLE IF NOT EXISTS temp.fileage(
972 @   fnid INTEGER PRIMARY KEY,
973 @   fid INTEGER,
974 @   mid INTEGER,
975 @   mtime DATETIME,
976 @   pathname TEXT
977 @ );
978 @ CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;
979 ;
980 
981 static const char zComputeFileAgeRun[] =
982 @ WITH RECURSIVE
983 @  ckin(x) AS (VALUES(:ckin)
984 @              UNION
985 @              SELECT plink.pid
986 @                FROM ckin, plink
987 @               WHERE plink.cid=ckin.x)
988 @ INSERT OR IGNORE INTO fileage(fnid, fid, mid, mtime, pathname)
989 @   SELECT filename.fnid, mlink.fid, mlink.mid, event.mtime, filename.name
990 @     FROM foci, filename, blob, mlink, event
991 @    WHERE foci.checkinID=:ckin
992 @      AND foci.filename GLOB :glob
993 @      AND filename.name=foci.filename
994 @      AND blob.uuid=foci.uuid
995 @      AND mlink.fid=blob.rid
996 @      AND mlink.fid!=mlink.pid
997 @      AND mlink.mid IN (SELECT x FROM ckin)
998 @      AND event.objid=mlink.mid
999 @  ORDER BY event.mtime ASC;
1000 ;
1001 
1002 /*
1003 ** Look at all file containing in the version "vid".  Construct a
1004 ** temporary table named "fileage" that contains the file-id for each
1005 ** files, the pathname, the check-in where the file was added, and the
1006 ** mtime on that check-in. If zGlob and *zGlob then only files matching
1007 ** the given glob are computed.
1008 */
compute_fileage(int vid,const char * zGlob)1009 int compute_fileage(int vid, const char* zGlob){
1010   Stmt q;
1011   db_exec_sql(zComputeFileAgeSetup);
1012   db_prepare(&q, zComputeFileAgeRun  /*works-like:"constant"*/);
1013   db_bind_int(&q, ":ckin", vid);
1014   db_bind_text(&q, ":glob", zGlob && zGlob[0] ? zGlob : "*");
1015   db_exec(&q);
1016   db_finalize(&q);
1017   return 0;
1018 }
1019 
1020 /*
1021 ** Render the number of days in rAge as a more human-readable time span.
1022 ** Different units (seconds, minutes, hours, days, months, years) are
1023 ** selected depending on the magnitude of rAge.
1024 **
1025 ** The string returned is obtained from fossil_malloc() and should be
1026 ** freed by the caller.
1027 */
human_readable_age(double rAge)1028 char *human_readable_age(double rAge){
1029   if( rAge*86400.0<120 ){
1030     if( rAge*86400.0<1.0 ){
1031       return mprintf("current");
1032     }else{
1033       return mprintf("%d seconds", (int)(rAge*86400.0));
1034     }
1035   }else if( rAge*1440.0<90 ){
1036     return mprintf("%.1f minutes", rAge*1440.0);
1037   }else if( rAge*24.0<36 ){
1038     return mprintf("%.1f hours", rAge*24.0);
1039   }else if( rAge<365.0 ){
1040     return mprintf("%.1f days", rAge);
1041   }else{
1042     return mprintf("%.2f years", rAge/365.2425);
1043   }
1044 }
1045 
1046 /*
1047 ** COMMAND: test-fileage
1048 **
1049 ** Usage: %fossil test-fileage CHECKIN
1050 */
test_fileage_cmd(void)1051 void test_fileage_cmd(void){
1052   int mid;
1053   Stmt q;
1054   const char *zGlob = find_option("glob",0,1);
1055   db_find_and_open_repository(0,0);
1056   verify_all_options();
1057   if( g.argc!=3 ) usage("CHECKIN");
1058   mid = name_to_typed_rid(g.argv[2],"ci");
1059   compute_fileage(mid, zGlob);
1060   db_prepare(&q,
1061     "SELECT fid, mid, julianday('now') - mtime, pathname"
1062     "  FROM fileage"
1063   );
1064   while( db_step(&q)==SQLITE_ROW ){
1065     char *zAge = human_readable_age(db_column_double(&q,2));
1066     fossil_print("%8d %8d %16s %s\n",
1067       db_column_int(&q,0),
1068       db_column_int(&q,1),
1069       zAge,
1070       db_column_text(&q,3));
1071     fossil_free(zAge);
1072   }
1073   db_finalize(&q);
1074 }
1075 
1076 /*
1077 ** WEBPAGE: fileage
1078 **
1079 ** Show all files in a single check-in (identified by the name= query
1080 ** parameter) in order of increasing age.
1081 **
1082 ** Parameters:
1083 **   name=VERSION   Selects the check-in version (default=tip).
1084 **   glob=STRING    Only shows files matching this glob pattern
1085 **                  (e.g. *.c or *.txt).
1086 **   showid         Show RID values for debugging
1087 */
fileage_page(void)1088 void fileage_page(void){
1089   int rid;
1090   const char *zName;
1091   const char *zGlob;
1092   const char *zUuid;
1093   const char *zNow;            /* Time of check-in */
1094   int isBranchCI;              /* name= is a branch name */
1095   int showId = PB("showid");
1096   Stmt q1, q2;
1097   double baseTime;
1098   login_check_credentials();
1099   if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
1100   if( exclude_spiders() ) return;
1101   zName = P("name");
1102   if( zName==0 ) zName = "tip";
1103   rid = symbolic_name_to_rid(zName, "ci");
1104   if( rid==0 ){
1105     fossil_fatal("not a valid check-in: %s", zName);
1106   }
1107   zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
1108   isBranchCI = branch_includes_uuid(zName,zUuid);
1109   baseTime = db_double(0.0,"SELECT mtime FROM event WHERE objid=%d", rid);
1110   zNow = db_text("", "SELECT datetime(mtime,toLocal()) FROM event"
1111                      " WHERE objid=%d", rid);
1112   style_submenu_element("Tree-View", "%R/tree?ci=%T&mtime=1&type=tree", zName);
1113   style_header("File Ages");
1114   zGlob = P("glob");
1115   compute_fileage(rid,zGlob);
1116   db_multi_exec("CREATE INDEX fileage_ix1 ON fileage(mid,pathname);");
1117 
1118   if( fossil_strcmp(zName,"tip")==0 ){
1119     @ <h1>Files in the %z(href("%R/info?name=tip"))latest check-in</a>
1120   }else if( isBranchCI ){
1121     @ <h1>Files in the %z(href("%R/info?name=%T",zName))latest check-in</a>
1122     @ of branch %z(href("%R/timeline?r=%T",zName))%h(zName)</a>
1123   }else{
1124     @ <h1>Files in check-in %z(href("%R/info?name=%T",zName))%h(zName)</a>
1125   }
1126   if( zGlob && zGlob[0] ){
1127     @ that match "%h(zGlob)"
1128   }
1129   @ ordered by age</h1>
1130   @
1131   @ <p>File ages are expressed relative to the check-in time of
1132   @ %z(href("%R/timeline?c=%t",zNow))%s(zNow)</a>.</p>
1133   @
1134   @ <div class='fileage'><table>
1135   @ <tr><th>Age</th><th>Files</th><th>Check-in</th></tr>
1136   db_prepare(&q1,
1137     "SELECT event.mtime, event.objid, blob.uuid,\n"
1138     "       coalesce(event.ecomment,event.comment),\n"
1139     "       coalesce(event.euser,event.user),\n"
1140     "       coalesce((SELECT value FROM tagxref\n"
1141     "                  WHERE tagtype>0 AND tagid=%d\n"
1142     "                    AND rid=event.objid),'trunk')\n"
1143     "  FROM event, blob\n"
1144     " WHERE event.objid IN (SELECT mid FROM fileage)\n"
1145     "   AND blob.rid=event.objid\n"
1146     " ORDER BY event.mtime DESC;",
1147     TAG_BRANCH
1148   );
1149   db_prepare(&q2,
1150     "SELECT filename.name, fileage.fid\n"
1151     "  FROM fileage, filename\n"
1152     " WHERE fileage.mid=:mid AND filename.fnid=fileage.fnid"
1153   );
1154   while( db_step(&q1)==SQLITE_ROW ){
1155     double age = baseTime - db_column_double(&q1, 0);
1156     int mid = db_column_int(&q1, 1);
1157     const char *zUuid = db_column_text(&q1, 2);
1158     const char *zComment = db_column_text(&q1, 3);
1159     const char *zUser = db_column_text(&q1, 4);
1160     const char *zBranch = db_column_text(&q1, 5);
1161     char *zAge = human_readable_age(age);
1162     @ <tr><td>%s(zAge)</td>
1163     @ <td>
1164     db_bind_int(&q2, ":mid", mid);
1165     while( db_step(&q2)==SQLITE_ROW ){
1166       const char *zFile = db_column_text(&q2,0);
1167       @ %z(href("%R/file?name=%T&ci=%!S",zFile,zUuid))%h(zFile)</a> \
1168       if( showId ){
1169         int fid = db_column_int(&q2,1);
1170         @ (%d(fid))<br />
1171       }else{
1172         @ </a><br />
1173       }
1174     }
1175     db_reset(&q2);
1176     @ </td>
1177     @ <td>
1178     @ %W(zComment)
1179     @ (check-in:&nbsp;%z(href("%R/info/%!S",zUuid))%S(zUuid)</a>,
1180     if( showId ){
1181       @ id: %d(mid)
1182     }
1183     @ user:&nbsp;%z(href("%R/timeline?u=%t&c=%!S&nd",zUser,zUuid))%h(zUser)</a>,
1184     @ branch:&nbsp;\
1185     @ %z(href("%R/timeline?r=%t&c=%!S&nd",zBranch,zUuid))%h(zBranch)</a>)
1186     @ </td></tr>
1187     @
1188     fossil_free(zAge);
1189   }
1190   @ </table></div>
1191   db_finalize(&q1);
1192   db_finalize(&q2);
1193   style_finish_page();
1194 }
1195