File Annotation
Not logged in
a48474bc75 2008-05-29       drh: /*
a48474bc75 2008-05-29       drh: ** Copyright (c) 2008 D. Richard Hipp
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: ** This program is free software; you can redistribute it and/or
a48474bc75 2008-05-29       drh: ** modify it under the terms of the GNU General Public
a48474bc75 2008-05-29       drh: ** License version 2 as published by the Free Software Foundation.
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: ** This program is distributed in the hope that it will be useful,
a48474bc75 2008-05-29       drh: ** but WITHOUT ANY WARRANTY; without even the implied warranty of
a48474bc75 2008-05-29       drh: ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
a48474bc75 2008-05-29       drh: ** General Public License for more details.
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: ** You should have received a copy of the GNU General Public
a48474bc75 2008-05-29       drh: ** License along with this library; if not, write to the
a48474bc75 2008-05-29       drh: ** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
a48474bc75 2008-05-29       drh: ** Boston, MA  02111-1307, USA.
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: ** Author contact information:
a48474bc75 2008-05-29       drh: **   drh@hwaci.com
a48474bc75 2008-05-29       drh: **   http://www.hwaci.com/drh/
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: *******************************************************************************
a48474bc75 2008-05-29       drh: **
a48474bc75 2008-05-29       drh: ** This file contains code used to manage SHUN table of the repository
a48474bc75 2008-05-29       drh: */
a48474bc75 2008-05-29       drh: #include "config.h"
a48474bc75 2008-05-29       drh: #include "shun.h"
a48474bc75 2008-05-29       drh: #include <assert.h>
a48474bc75 2008-05-29       drh: 
a48474bc75 2008-05-29       drh: /*
766bec08ce 2009-01-25       drh: ** Return true if the given artifact ID should be shunned.
a48474bc75 2008-05-29       drh: */
a48474bc75 2008-05-29       drh: int uuid_is_shunned(const char *zUuid){
a48474bc75 2008-05-29       drh:   static Stmt q;
a48474bc75 2008-05-29       drh:   int rc;
a48474bc75 2008-05-29       drh:   if( zUuid==0 || zUuid[0]==0 ) return 0;
a48474bc75 2008-05-29       drh:   db_static_prepare(&q, "SELECT 1 FROM shun WHERE uuid=:uuid");
a48474bc75 2008-05-29       drh:   db_bind_text(&q, ":uuid", zUuid);
a48474bc75 2008-05-29       drh:   rc = db_step(&q);
a48474bc75 2008-05-29       drh:   db_reset(&q);
a48474bc75 2008-05-29       drh:   return rc==SQLITE_ROW;
a48474bc75 2008-05-29       drh: }
a48474bc75 2008-05-29       drh: 
a48474bc75 2008-05-29       drh: /*
a48474bc75 2008-05-29       drh: ** WEBPAGE: shun
a48474bc75 2008-05-29       drh: */
a48474bc75 2008-05-29       drh: void shun_page(void){
a48474bc75 2008-05-29       drh:   Stmt q;
a48474bc75 2008-05-29       drh:   int cnt = 0;
a48474bc75 2008-05-29       drh:   const char *zUuid = P("uuid");
a48474bc75 2008-05-29       drh:   int nUuid;
a48474bc75 2008-05-29       drh:   char zCanonical[UUID_SIZE+1];
a48474bc75 2008-05-29       drh: 
a48474bc75 2008-05-29       drh:   login_check_credentials();
a48474bc75 2008-05-29       drh:   if( !g.okAdmin ){
a48474bc75 2008-05-29       drh:     login_needed();
a48474bc75 2008-05-29       drh:   }
3f6edbc779 2008-11-10       drh:   if( P("rebuild") ){
8040619968 2008-11-27       drh:     db_close();
8040619968 2008-11-27       drh:     db_open_repository(g.zRepositoryName);
3f6edbc779 2008-11-10       drh:     db_begin_transaction();
3f6edbc779 2008-11-10       drh:     rebuild_db(0,0);
3f6edbc779 2008-11-10       drh:     db_end_transaction(0);
0be54823ba 2008-10-18       drh:   }
a48474bc75 2008-05-29       drh:   if( zUuid ){
a48474bc75 2008-05-29       drh:     nUuid = strlen(zUuid);
a48474bc75 2008-05-29       drh:     if( nUuid!=40 || !validate16(zUuid, nUuid) ){
a48474bc75 2008-05-29       drh:       zUuid = 0;
a48474bc75 2008-05-29       drh:     }else{
a48474bc75 2008-05-29       drh:       memcpy(zCanonical, zUuid, UUID_SIZE+1);
a48474bc75 2008-05-29       drh:       canonical16(zCanonical, UUID_SIZE);
a48474bc75 2008-05-29       drh:       zUuid = zCanonical;
a48474bc75 2008-05-29       drh:     }
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   style_header("Shunned Artifacts");
a48474bc75 2008-05-29       drh:   if( zUuid && P("sub") ){
0be54823ba 2008-10-18       drh:     login_verify_csrf_secret();
a48474bc75 2008-05-29       drh:     db_multi_exec("DELETE FROM shun WHERE uuid='%s'", zUuid);
a48474bc75 2008-05-29       drh:     if( db_exists("SELECT 1 FROM blob WHERE uuid='%s'", zUuid) ){
a48474bc75 2008-05-29       drh:       @ <p><font color="blue">Artifact
a48474bc75 2008-05-29       drh:       @ <a href="%s(g.zBaseURL)/artifact/%s(zUuid)">%s(zUuid)</a> is no
a48474bc75 2008-05-29       drh:       @ longer being shunned.</font></p>
a48474bc75 2008-05-29       drh:     }else{
a48474bc75 2008-05-29       drh:       @ <p><font color="blue">Artifact %s(zUuid)</a> will no longer
a48474bc75 2008-05-29       drh:       @ be shunned.  But it does not exist in the repository.  It
a48474bc75 2008-05-29       drh:       @ may be necessary to rebuild the repository using the
a48474bc75 2008-05-29       drh:       @ <b>fossil rebuild</b> command-line before the artifact content
a48474bc75 2008-05-29       drh:       @ can pulled in from other respositories.</font></p>
a48474bc75 2008-05-29       drh:     }
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   if( zUuid && P("add") ){
0be54823ba 2008-10-18       drh:     login_verify_csrf_secret();
a48474bc75 2008-05-29       drh:     db_multi_exec("INSERT OR IGNORE INTO shun VALUES('%s')", zUuid);
a48474bc75 2008-05-29       drh:     @ <p><font color="blue">Artifact
a48474bc75 2008-05-29       drh:     @ <a href="%s(g.zBaseURL)/artifact/%s(zUuid)">%s(zUuid)</a> has been
a48474bc75 2008-05-29       drh:     @ shunned.  It will no longer be pushed.
a48474bc75 2008-05-29       drh:     @ It will be removed from the repository the next time the respository
a48474bc75 2008-05-29       drh:     @ is rebuilt using the <b>fossil rebuild</b> command-line</font></p>
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   @ <p>The artifacts listed below have been shunned by this repository.
a48474bc75 2008-05-29       drh:   @ This means that the artifacts will not be transmitted on a push nor
a48474bc75 2008-05-29       drh:   @ recieved on a pull.  These artifacts are banned from the respository.</p>
a48474bc75 2008-05-29       drh:   @ <blockquote>
a48474bc75 2008-05-29       drh:   db_prepare(&q,
a48474bc75 2008-05-29       drh:      "SELECT uuid, EXISTS(SELECT 1 FROM blob WHERE blob.uuid=shun.uuid)"
a48474bc75 2008-05-29       drh:      "  FROM shun ORDER BY uuid");
a48474bc75 2008-05-29       drh:   while( db_step(&q)==SQLITE_ROW ){
a48474bc75 2008-05-29       drh:     const char *zUuid = db_column_text(&q, 0);
a48474bc75 2008-05-29       drh:     int stillExists = db_column_int(&q, 1);
a48474bc75 2008-05-29       drh:     cnt++;
a48474bc75 2008-05-29       drh:     if( stillExists ){
a48474bc75 2008-05-29       drh:       @ <b><a href="%s(g.zBaseURL)/artifact/%s(zUuid)">%s(zUuid)</a></b><br>
a48474bc75 2008-05-29       drh:     }else{
a48474bc75 2008-05-29       drh:       @ <b>%s(zUuid)</b><br>
a48474bc75 2008-05-29       drh:     }
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   if( cnt==0 ){
a48474bc75 2008-05-29       drh:     @ <i>no artifacts are shunned on this server</i>
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   db_finalize(&q);
a48474bc75 2008-05-29       drh:   @ </blockquote>
a48474bc75 2008-05-29       drh:   @ <hr>
94a93469c8 2008-06-02       drh:   @ <a name="addshun"></a>
766bec08ce 2009-01-25       drh:   @ <p>To shun an artifact, enter its artifact ID (the 40-character SHA1
766bec08ce 2009-01-25       drh:   @ hash of the artifact) in the
a48474bc75 2008-05-29       drh:   @ following box and press the "Shun" button.  This will cause the artifact
a48474bc75 2008-05-29       drh:   @ to be removed from the repository and will prevent the artifact from being
a48474bc75 2008-05-29       drh:   @ readded to the repository by subsequent sync operation.</p>
766bec08ce 2009-01-25       drh:   @
766bec08ce 2009-01-25       drh:   @ <p>Note that you must enter the full 40-character artifact ID, not
766bec08ce 2009-01-25       drh:   @ an abbreviation or a symbolic tag.</p>
a48474bc75 2008-05-29       drh:   @
a48474bc75 2008-05-29       drh:   @ <p>Warning:  Shunning should only be used to remove inappropriate content
a48474bc75 2008-05-29       drh:   @ from the repository.  Inappropriate content includes such things as
a48474bc75 2008-05-29       drh:   @ spam added to Wiki, files that violate copyright or patent agreements,
a48474bc75 2008-05-29       drh:   @ or artifacts that by design or accident interfere with the processing
a48474bc75 2008-05-29       drh:   @ of the repository.  Do not shun artifacts merely to remove them from
a48474bc75 2008-05-29       drh:   @ sight - set the "hidden" tag on such artifacts instead.</p>
a48474bc75 2008-05-29       drh:   @
a48474bc75 2008-05-29       drh:   @ <blockquote>
a48474bc75 2008-05-29       drh:   @ <form method="POST" action="%s(g.zBaseURL)/%s(g.zPath)">
0be54823ba 2008-10-18       drh:   login_insert_csrf_secret();
94a93469c8 2008-06-02       drh:   @ <input type="text" name="uuid" value="%h(PD("shun",""))" size="50">
a48474bc75 2008-05-29       drh:   @ <input type="submit" name="add" value="Shun">
a48474bc75 2008-05-29       drh:   @ </form>
a48474bc75 2008-05-29       drh:   @ </blockquote>
a48474bc75 2008-05-29       drh:   @
a48474bc75 2008-05-29       drh:   @ <p>Enter the UUID of a previous shunned artifact to cause it to be
a48474bc75 2008-05-29       drh:   @ accepted again in the repository.  The artifact content is not
a48474bc75 2008-05-29       drh:   @ restored because the content is unknown.  The only change is that
a48474bc75 2008-05-29       drh:   @ the formerly shunned artifact will be accepted on subsequent sync
a48474bc75 2008-05-29       drh:   @ operations.</p>
a48474bc75 2008-05-29       drh:   @
a48474bc75 2008-05-29       drh:   @ <blockquote>
a48474bc75 2008-05-29       drh:   @ <form method="POST" action="%s(g.zBaseURL)/%s(g.zPath)">
0be54823ba 2008-10-18       drh:   login_insert_csrf_secret();
a48474bc75 2008-05-29       drh:   @ <input type="text" name="uuid" size="50">
a48474bc75 2008-05-29       drh:   @ <input type="submit" name="sub" value="Accept">
a48474bc75 2008-05-29       drh:   @ </form>
a48474bc75 2008-05-29       drh:   @ </blockquote>
3f6edbc779 2008-11-10       drh:   @
766bec08ce 2009-01-25       drh:   @ <hr>
3f6edbc779 2008-11-10       drh:   @ <p>Press the button below to rebuild the respository.  The rebuild
3f6edbc779 2008-11-10       drh:   @ may take several seconds, so be patient after pressing the button.</p>
3f6edbc779 2008-11-10       drh:   @
3f6edbc779 2008-11-10       drh:   @ <blockquote>
3f6edbc779 2008-11-10       drh:   @ <form method="POST" action="%s(g.zBaseURL)/%s(g.zPath)">
3f6edbc779 2008-11-10       drh:   login_insert_csrf_secret();
3f6edbc779 2008-11-10       drh:   @ <input type="submit" name="rebuild" value="Rebuild">
3f6edbc779 2008-11-10       drh:   @ </form>
3f6edbc779 2008-11-10       drh:   @ </blockquote>
3f6edbc779 2008-11-10       drh:   @
a48474bc75 2008-05-29       drh:   style_footer();
a48474bc75 2008-05-29       drh: }
a48474bc75 2008-05-29       drh: 
a48474bc75 2008-05-29       drh: /*
a48474bc75 2008-05-29       drh: ** Remove from the BLOB table all artifacts that are in the SHUN table.
a48474bc75 2008-05-29       drh: */
a48474bc75 2008-05-29       drh: void shun_artifacts(void){
a48474bc75 2008-05-29       drh:   Stmt q;
a48474bc75 2008-05-29       drh:   db_multi_exec(
a48474bc75 2008-05-29       drh:      "CREATE TEMP TABLE toshun(rid INTEGER PRIMARY KEY);"
a48474bc75 2008-05-29       drh:      "INSERT INTO toshun SELECT rid FROM blob, shun WHERE blob.uuid=shun.uuid;"
a48474bc75 2008-05-29       drh:   );
a48474bc75 2008-05-29       drh:   db_prepare(&q,
a48474bc75 2008-05-29       drh:      "SELECT rid FROM delta WHERE srcid IN toshun"
a48474bc75 2008-05-29       drh:   );
a48474bc75 2008-05-29       drh:   while( db_step(&q)==SQLITE_ROW ){
a48474bc75 2008-05-29       drh:     int srcid = db_column_int(&q, 0);
a48474bc75 2008-05-29       drh:     content_undelta(srcid);
a48474bc75 2008-05-29       drh:   }
a48474bc75 2008-05-29       drh:   db_finalize(&q);
a48474bc75 2008-05-29       drh:   db_multi_exec(
a48474bc75 2008-05-29       drh:      "DELETE FROM delta WHERE rid IN toshun;"
a48474bc75 2008-05-29       drh:      "DELETE FROM blob WHERE rid IN toshun;"
a48474bc75 2008-05-29       drh:      "DROP TABLE toshun;"
a48474bc75 2008-05-29       drh:   );
766bec08ce 2009-01-25       drh: }
766bec08ce 2009-01-25       drh: 
766bec08ce 2009-01-25       drh: /*
766bec08ce 2009-01-25       drh: ** WEBPAGE: rcvfromlist
766bec08ce 2009-01-25       drh: **
766bec08ce 2009-01-25       drh: ** Show a listing of RCVFROM table entries.
766bec08ce 2009-01-25       drh: */
766bec08ce 2009-01-25       drh: void rcvfromlist_page(void){
766bec08ce 2009-01-25       drh:   int ofst = atoi(PD("ofst","0"));
766bec08ce 2009-01-25       drh:   int cnt;
766bec08ce 2009-01-25       drh:   Stmt q;
766bec08ce 2009-01-25       drh: 
766bec08ce 2009-01-25       drh:   login_check_credentials();
766bec08ce 2009-01-25       drh:   if( !g.okAdmin ){
766bec08ce 2009-01-25       drh:     login_needed();
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   style_header("Content Sources");
766bec08ce 2009-01-25       drh:   if( ofst>0 ){
766bec08ce 2009-01-25       drh:     style_submenu_element("Later", "Later", "rcvfromlist?ofst=%d",
766bec08ce 2009-01-25       drh:                            ofst>30 ? ofst-30 : 0);
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   db_prepare(&q,
766bec08ce 2009-01-25       drh:     "SELECT rcvid, login, datetime(rcvfrom.mtime), rcvfrom.ipaddr"
766bec08ce 2009-01-25       drh:     "  FROM rcvfrom LEFT JOIN user USING(uid)"
766bec08ce 2009-01-25       drh:     " ORDER BY rcvid DESC LIMIT 31 OFFSET %d",
766bec08ce 2009-01-25       drh:     ofst
766bec08ce 2009-01-25       drh:   );
9c89b0e0f1 2009-01-25       drh:   @ <p>Whenever new artifacts are added to the repository, either by
9c89b0e0f1 2009-01-25       drh:   @ push or using the web interface, an entry is made in the RCVFROM table
9c89b0e0f1 2009-01-25       drh:   @ to record the source of that artifact.  This log facilitates
9c89b0e0f1 2009-01-25       drh:   @ finding and fixing attempts to inject illicit content into the
9c89b0e0f1 2009-01-25       drh:   @ repository.</p>
9c89b0e0f1 2009-01-25       drh:   @
9c89b0e0f1 2009-01-25       drh:   @ <p>Click on the "rcvid" to show a list of specific artifacts received
9c89b0e0f1 2009-01-25       drh:   @ by a transaction.  After identifying illicit artifacts, remove them
9c89b0e0f1 2009-01-25       drh:   @ using the "Shun" feature.</p>
9c89b0e0f1 2009-01-25       drh:   @
766bec08ce 2009-01-25       drh:   @ <table cellpadding=0 cellspacing=0 border=0>
766bec08ce 2009-01-25       drh:   @ <tr><th>rcvid</th><th width=15>
766bec08ce 2009-01-25       drh:   @     <th>Date</th><th width=15><th>User</th>
766bec08ce 2009-01-25       drh:   @     <th width=15><th>IP&nbsp;Address</th></tr>
766bec08ce 2009-01-25       drh:   cnt = 0;
766bec08ce 2009-01-25       drh:   while( db_step(&q)==SQLITE_ROW ){
766bec08ce 2009-01-25       drh:     int rcvid = db_column_int(&q, 0);
766bec08ce 2009-01-25       drh:     const char *zUser = db_column_text(&q, 1);
766bec08ce 2009-01-25       drh:     const char *zDate = db_column_text(&q, 2);
766bec08ce 2009-01-25       drh:     const char *zIpAddr = db_column_text(&q, 3);
766bec08ce 2009-01-25       drh:     if( cnt==30 ){
766bec08ce 2009-01-25       drh:       style_submenu_element("Earlier", "Earlier",
766bec08ce 2009-01-25       drh:          "rcvfromlist?ofst=%d", ofst+30);
766bec08ce 2009-01-25       drh:     }else{
766bec08ce 2009-01-25       drh:       cnt++;
766bec08ce 2009-01-25       drh:       @ <tr>
766bec08ce 2009-01-25       drh:       @ <td><a href="rcvfrom?rcvid=%d(rcvid)">%d(rcvid)</a></td><td>
766bec08ce 2009-01-25       drh:       @ <td>%s(zDate)</td><td>
766bec08ce 2009-01-25       drh:       @ <td>%h(zUser)</td><td>
766bec08ce 2009-01-25       drh:       @ <td>&nbsp;%s(zIpAddr)&nbsp</td>
766bec08ce 2009-01-25       drh:       @ </tr>
766bec08ce 2009-01-25       drh:     }
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   db_finalize(&q);
766bec08ce 2009-01-25       drh:   @ </table>
766bec08ce 2009-01-25       drh:   style_footer();
766bec08ce 2009-01-25       drh: }
766bec08ce 2009-01-25       drh: 
766bec08ce 2009-01-25       drh: /*
766bec08ce 2009-01-25       drh: ** WEBPAGE: rcvfrom
766bec08ce 2009-01-25       drh: **
766bec08ce 2009-01-25       drh: ** Show a single RCVFROM table entry.
766bec08ce 2009-01-25       drh: */
766bec08ce 2009-01-25       drh: void rcvfrom_page(void){
766bec08ce 2009-01-25       drh:   int rcvid = atoi(PD("rcvid","0"));
766bec08ce 2009-01-25       drh:   Stmt q;
766bec08ce 2009-01-25       drh: 
766bec08ce 2009-01-25       drh:   login_check_credentials();
766bec08ce 2009-01-25       drh:   if( !g.okAdmin ){
766bec08ce 2009-01-25       drh:     login_needed();
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   style_header("Content Source %d", rcvid);
766bec08ce 2009-01-25       drh:   db_prepare(&q,
766bec08ce 2009-01-25       drh:     "SELECT login, datetime(rcvfrom.mtime), rcvfrom.ipaddr"
766bec08ce 2009-01-25       drh:     "  FROM rcvfrom LEFT JOIN user USING(uid)"
766bec08ce 2009-01-25       drh:     " WHERE rcvid=%d",
766bec08ce 2009-01-25       drh:     rcvid
766bec08ce 2009-01-25       drh:   );
766bec08ce 2009-01-25       drh:   @ <table cellspacing=15 cellpadding=0 border=0>
9c89b0e0f1 2009-01-25       drh:   @ <tr><td valign="top" align="right"><b>rcvid:</b></td>
766bec08ce 2009-01-25       drh:   @ <td valign="top">%d(rcvid)</td></tr>
766bec08ce 2009-01-25       drh:   if( db_step(&q)==SQLITE_ROW ){
766bec08ce 2009-01-25       drh:     const char *zUser = db_column_text(&q, 0);
766bec08ce 2009-01-25       drh:     const char *zDate = db_column_text(&q, 1);
766bec08ce 2009-01-25       drh:     const char *zIpAddr = db_column_text(&q, 2);
9c89b0e0f1 2009-01-25       drh:     @ <tr><td valign="top" align="right"><b>User:</b></td>
766bec08ce 2009-01-25       drh:     @ <td valign="top">%s(zUser)</td></tr>
9c89b0e0f1 2009-01-25       drh:     @ <tr><td valign="top" align="right"><b>Date:</b></td>
766bec08ce 2009-01-25       drh:     @ <td valign="top">%s(zDate)</td></tr>
9c89b0e0f1 2009-01-25       drh:     @ <tr><td valign="top" align="right"><b>IP&nbsp;Address:</b></td>
766bec08ce 2009-01-25       drh:     @ <td valign="top">%s(zIpAddr)</td></tr>
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   db_finalize(&q);
766bec08ce 2009-01-25       drh:   db_prepare(&q,
766bec08ce 2009-01-25       drh:     "SELECT rid, uuid, size FROM blob WHERE rcvid=%d", rcvid
766bec08ce 2009-01-25       drh:   );
9c89b0e0f1 2009-01-25       drh:   @ <tr><td valign="top" align="right"><b>Artifacts:</b></td>
766bec08ce 2009-01-25       drh:   @ <td valign="top">
766bec08ce 2009-01-25       drh:   while( db_step(&q)==SQLITE_ROW ){
766bec08ce 2009-01-25       drh:     int rid = db_column_int(&q, 0);
766bec08ce 2009-01-25       drh:     const char *zUuid = db_column_text(&q, 1);
766bec08ce 2009-01-25       drh:     int size = db_column_int(&q, 2);
766bec08ce 2009-01-25       drh:     @ <a href="%s(g.zBaseURL)/info/%s(zUuid)">%s(zUuid)</a>
766bec08ce 2009-01-25       drh:     @ (rid: %d(rid), size: %d(size))<br>
766bec08ce 2009-01-25       drh:   }
766bec08ce 2009-01-25       drh:   @ </td></tr>
766bec08ce 2009-01-25       drh:   @ </table>
a48474bc75 2008-05-29       drh: }