/*
** Copyright (c) 2002 D. Richard Hipp
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the GNU General Public
** License as published by the Free Software Foundation; either
** version 2 of the License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** General Public License for more details.
**
** You should have received a copy of the GNU General Public
** License along with this library; if not, write to the
** Free Software Foundation, Inc., 59 Temple Place - Suite 330,
** Boston, MA 02111-1307, USA.
**
** Author contact information:
** drh@hwaci.com
** http://www.hwaci.com/drh/
**
*******************************************************************************
**
** Code for handling attachments
*/
#include "config.h"
#include "attach.h"
#include <time.h>
/*
** The default maximum size of an attachment. This value can be changed
** by the setup users.
*/
#define MX_ATTACH_SIZE "102400"
/*
** Return the maximum size of an attachment in bytes
*/
int attachment_max(void){
return atoi(db_config("max_attach_size",MX_ATTACH_SIZE));
}
static int is_integer(const char *zString){
if( zString==0 ) return 0;
while( *zString ){
if( !isdigit(*zString) ) return 0;
zString ++;
}
return 1;
}
/*
** WEBPAGE: /attach_add
**
** This web-page gives the user the opportunity to add an attachment to
** an existing ticket. The "tn" query parameter specifies the ticket
** number. A tn of zero (an invalid ticket number) means attachments to
** the setup page (i.e. stylesheets, logos, etc).
**
** This routine has been extended so that "tn" can now be the name of
** a Wiki page. This allows attachments to be added to wiki.
*/
void attachment_add(void){
const char *zPage;
char *zTitle = NULL;
char *zErr = 0;
const char *zBack;
int mxSize = attachment_max();
int tn = atoi(PD("tn","0"));
int isHome = 0;
zPage = P("tn");
if( zPage==0 ){
common_err("Invalid or missing \"tn\" query parameter");
}
login_check_credentials();
throttle(1,1);
if( is_integer(zPage) ){
if( tn ){
zBack = mprintf("tktview?tn=%d", tn);
if( !g.okWrite ){
cgi_redirect(zBack);
}
}else{
zBack = "setup_style";
if( !g.okSetup ){
cgi_redirect("index");
}
}
}else{
isHome = is_user_page(zPage) && is_home_page(zPage);
if( !isHome && is_wiki_name(zPage)!=strlen(zPage) ){
common_err("Invalid wiki page name \"tn=%h\"", zPage);
}
zBack = mprintf("wiki?p=%t", zPage);
if( !g.okWiki ){
cgi_redirect(zBack);
}
}
common_add_help_item("CvstracAttachment");
common_add_action_item(zBack, "Cancel");
if( P("can") || mxSize<=0 ){
cgi_redirect(zBack);
}
if( P("ok") ){
const char *zData = P("f");
const char *zDescription = PD("d","");
const char *zName = P("f:filename");
int size = atoi(PD("f:bytes","0"));
const char *zType = PD("f:mimetype","text/plain");
const char *z;
time_t now = time(0);
char **az;
int atn;
if( zData==0 || zName==0 || size==0 || zType==0 ){
common_err("Attachment information is missing from the query content");
}
if( size>mxSize ){
zErr = mprintf("Your attachment is too big. The maximum allowed size "
"is %dKB but your attachment was %dKB", mxSize/1024,
(size+1023)/1024);
}else{
sqlite3 *pDb;
sqlite3_stmt *pStmt;
const char *zTail;
int rc;
for(z=zName; *z; z++){
if( (*z=='/' || *z=='\\') && z[1]!=0 ){ zName = &z[1]; }
}
/*
** In order to insert a blob, we need to drop down to raw SQLite 3
** calls.
*/
pDb = db_open();
rc = sqlite3_prepare( pDb,
"INSERT INTO "
" attachment(atn,tn,size,date,user,description,mime,fname,content) "
"VALUES(NULL,?,?,?,?,?,?,?,?);",
-1, &pStmt, &zTail);
if( rc!=SQLITE_OK ) {
db_err( sqlite3_errmsg(pDb), 0,
"/attach_add: unable to add \"%h\"", zName );
}
sqlite3_bind_text(pStmt, 1, zPage, -1, SQLITE_STATIC);
sqlite3_bind_int(pStmt, 2, size);
sqlite3_bind_int(pStmt, 3, now);
sqlite3_bind_text(pStmt, 4, g.zUser, -1, SQLITE_STATIC);
sqlite3_bind_text(pStmt, 5, zDescription, -1, SQLITE_STATIC);
sqlite3_bind_text(pStmt, 6, zType, -1, SQLITE_STATIC);
sqlite3_bind_text(pStmt, 7, zName, -1, SQLITE_STATIC);
sqlite3_bind_blob(pStmt, 8, zData, size, SQLITE_STATIC);
rc = sqlite3_step(pStmt);
if( rc!=SQLITE_DONE ) {
db_err( sqlite3_errmsg(pDb), 0,
"/attach_add: unable to add \"%h\"", zName );
}
sqlite3_finalize(pStmt);
az = db_query(
"SELECT MAX(ROWID) FROM attachment"
);
atn = atoi(az[0]);
ticket_notify(atoi(zPage), 0, 0, atn);
cgi_redirect(zBack);
}
}
if( is_integer(zPage) ){
if( tn==0 ){
/* FIXME: Not sure we need a separate page unless there's an error... */
common_header("Attachments To Setup");
}else{
zTitle = db_short_query("SELECT title FROM ticket WHERE tn=%d", tn);
if( zTitle==0 ){ common_err("No such ticket: #%d", tn); }
common_header("Attachments To Ticket #%d", tn);
}
}else{
common_header("Attachments to %h", wiki_expand_name(zPage));
}
if( zErr ){
cgi_printf("<p><font color=\"red\"><b>Error:</b> %h</font></p>\n",zErr);
}
if( is_integer(zPage) && tn ){
cgi_printf("<h2>Ticket #%d: %h</h2>\n",tn,zTitle);
}
if( attachment_html(zPage, "<p>Existing attachments:</p>", "")==0 ){
cgi_printf("<p>There are currently no attachments on this document.</p>\n");
}
cgi_printf("<p>To add a new attachment \n"
"select the file to attach below an press the \"Add Attachment\" button.\n"
"Attachments may not be larger than %dKB.</p>\n"
"<form method=\"POST\" action=\"attach_add\" enctype=\"multipart/form-data\">\n"
"File to attach: <input type=\"file\" name=\"f\"><br>\n"
"Description:\n"
"(<small>See <a href=\"#format_hints\">formatting hints</a></small>)<br>\n"
"<textarea name=\"d\" rows=\"4\" cols=\"70\" wrap=\"virtual\">\n"
"</textarea><br>\n"
"<input type=\"submit\" name=\"ok\" value=\"Add Attachment\">\n"
"<input type=\"submit\" name=\"can\" value=\"Cancel\">\n"
"<input type=\"hidden\" name=\"tn\" value=\"%h\">\n"
"</form>\n"
"<hr>\n"
"<a name=\"format_hints\">\n"
"<h3>Formatting Hints:</h3>\n",mxSize/1024,zPage);
append_formatting_hints();
common_footer();
}
static int output_attachment_callback(
int *nGot, /* Set if we got results */
int nArg, /* Number of columns in this result row */
char **azArg, /* Text of data in all columns */
char **azName /* Names of the columns */
){
if( nArg != 4 ) return 0;
(*nGot) ++;
cgi_set_content_type(azArg[1]);
cgi_modified_since(atoi(azArg[3]));
cgi_append_header(
mprintf("Last-Modified: %s\r\n",cgi_rfc822_datestamp(atoi(azArg[3]))));
cgi_append_content(azArg[2], atoi(azArg[0]));
g.isConst = 1;
return 0;
}
void attachment_output(int atn){
/*
** We need to use a callback here since the content is a BLOB type object
** and the usual db_query() won't handle NUL characters in a returned
** row. The callback has the full row buffer available and will handle
** all the output duties. got will be set if we get a row.
*/
int got = 0;
db_callback_query( output_attachment_callback, &got,
"SELECT size, mime, content, date "
"FROM attachment "
"WHERE atn=%d", atn);
if( !got ){
common_err("No such attachment: %d", atn);
}
}
/*
** WEBPAGE: /attach_get
**
** Retrieve an attachment. g.zExtra looks something like "90/file.gif", which
** the atoi() call turns into just the integer "90". The filename is ignored,
** although some browsers use it as an initial name when saving to disk.
*/
void attachment_get(void){
int atn = g.zExtra ? atoi(g.zExtra) : 0;
char *z;
login_check_credentials();
throttle(1,0);
if( atn==0 ) common_err("No attachment specified");
z = db_short_query("SELECT tn FROM attachment WHERE atn=%d", atn);
if( z && z[0] ){
if( is_integer(z) ){
if( !g.okRead ){ login_needed(); return; }
}else{
if( !g.okRdWiki ){ login_needed(); return; }
}
attachment_output(atn);
}else{
common_err("No attachment specified");
}
}
/*
** Return true if it is ok to delete an attachment created by zUser
** at time addTime. Rules:
**
** * The Setup user can delete any attachment no matter who added
** it or how old it is.
**
** * Any registered user can delete an attachment that they
** themselves created less than 24 hours ago.
**
** * Users with Delete privilege can delete an attachment added
** by anonymous within the past 24 hours.
**
*/
int ok_to_delete_attachment(int addTime, const char *zUser){
if( g.okSetup ){
return 1;
}
if( addTime<time(0)-86400 ){
return 0;
}
if( !g.isAnon && strcmp(zUser, g.zUser)==0 ){
return 1;
}
if( g.okDelete && strcmp(zUser, "anonymous")==0 ){
return 1;
}
return 0;
}
/*
** WEBPAGE: /attach_del
**
** Delete an attachment
*/
void attachment_delete(void){
int atn = atoi(PD("atn","0"));
char *zDocView;
struct tm *pTm;
time_t t;
char **az;
char zDate[200];
int isHome = 0;
login_check_credentials();
throttle(1,1);
az = db_query("SELECT tn, size, date, user, mime, fname, description "
"FROM attachment WHERE atn=%d", atn);
if( az[0]==0 ){
if( !g.okDelete ){
common_err("Access denied");
}else{
common_err("No such attachment: %d", atn);
}
}
isHome = is_user_page(az[0]) && is_home_page(az[0]);
t = atoi(az[2]);
if( !isHome && !ok_to_delete_attachment(t, az[3]) ){
common_err("Access denied");
}
if( is_integer(az[0]) ){
if( atoi(az[0]) ){
zDocView = mprintf("tktview?tn=%t",az[0]);
}else{
if( !g.okSetup ){
common_err("Access denied");
}
zDocView = "setup_style";
}
}else{
zDocView = mprintf("wiki?p=%t",az[0]);
}
if( P("can") ){
cgi_redirect(zDocView);
return;
}
if( P("ok") ){
db_execute("DELETE FROM attachment WHERE atn=%d", atn);
cgi_redirect(zDocView);
return;
}
common_add_action_item(zDocView, "Cancel");
common_add_help_item("CvstracAttachment");
pTm = gmtime(&t);
strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
common_header("Delete Attachment?");
cgi_printf("<p>Are you sure you want to delete this attachments?</p>\n"
"<blockquote>\n"
"%h %h bytes added by %h on %h UTC.\n",az[5],az[1],az[3],zDate);
if(az[6] && az[6][0]){
cgi_printf("<br>\n");
output_formatted(az[6], NULL);
cgi_printf("<br>\n");
}
cgi_printf("</blockquote>\n"
"\n"
"<form method=\"GET\" action=\"attach_del\">\n"
"<input type=\"hidden\" name=\"atn\" value=\"%d\">\n"
" \n"
"<input type=\"submit\" name=\"ok\" value=\"Yes, Delete\">\n"
" \n"
"<input type=\"submit\" name=\"can\" value=\"No, Cancel\">\n"
"</form>\n",atn);
common_footer();
}
/*
** This routine generates HTML that shows a list of attachments for
** the given ticket number or wiki page. If there are no attachments,
** nothing is generated. Return the number of attachments.
*/
int attachment_html(const char *zPage, const char *zBefore, const char *zAfter){
char **az;
int i = 0;
time_t now;
if( is_integer(zPage) ){
if( !g.okRead ) return 0;
}else{
if( !g.okRdWiki ) return 0;
}
az = db_query("SELECT atn, size, date, user, mime, fname, description "
"FROM attachment WHERE tn='%q' ORDER BY date", zPage);
time(&now);
if( az[0] ){
cgi_printf("%s\n"
"<ul>\n",zBefore);
for(i=0; az[i]; i+=7){
int atn = atoi(az[i]);
char zDate[200];
struct tm *pTm;
time_t t = atoi(az[i+2]);
pTm = gmtime(&t);
strftime(zDate, sizeof(zDate), "%Y-%b-%d %H:%M:%S", pTm);
cgi_printf("<li><a href=\"attach_get/%d/%t\">%h</a>\n"
"%h bytes added by %h on %h UTC.\n",atn,az[i+5],az[i+5],az[i+1],az[i+3],zDate);
if(az[i+6] && az[i+6][0]){
cgi_printf("<br>\n");
output_formatted(az[i+6], NULL);
cgi_printf("<br>\n");
}
if( ok_to_delete_attachment(t, az[i+3]) ){
cgi_printf("[<a href=\"attach_del?atn=%d\">delete</a>]\n",atn);
}
}
cgi_printf("</ul>\n"
"%s\n",zAfter);
}
db_query_free(az);
return i/7;
}
syntax highlighted by Code2HTML, v. 0.9.1