/**
* feedback.c
*
* Copyright (C) 2004 - 2008 Bojan Smojver, Rexursive
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
#include
#ifdef HAVE_RXV_SPIN_H
#include
#endif
#ifdef HAVE_MAGIC_H
#include
#endif
#ifdef HAVE_LIBESMTP_H
#include
#endif
#ifdef HAVE_APR_BASE64_H
#include
#endif
#ifdef HAVE_HTTP_LOG_H
#include
#endif
#define BUF_SIZE 80
#define B64_SIZE 57
/* something we can always assign as read only */
static const char *empty="";
/* something to carry around with the context */
typedef struct{
apr_table_t *params; /* parsed parameters */
apr_table_t *uploads; /* file uploads */
ap_regex_t *preg; /* compiled regex for e-mail validation */
magic_t magic; /* MIME magic cookie */
const char *column; /* column to match within rows */
const char *marker; /* column to select within rows */
rxv_spin_data_t *rows; /* rows to match/select */
} feedback_extra_t;
/* apr_table_do: set parameters back into context */
static int ctx_set(void *rec,const char *key,const char *val){
rxv_spin_sset(rec,key,val);
return 1;
}
/* apr_table_do: select values from a list, multiple */
static int list_select(void *rec,const char *key,const char *val){
rxv_spin_ctx_t *ctx=rec;
feedback_extra_t *extra=rxv_spin_xget(ctx);
apr_pool_t *pool=rxv_spin_pool(ctx);
rxv_spin_data_t *d;
rxv_spin_curs_t *c=NULL;
apr_size_t i;
for(i=rxv_spin_first(pool,extra->rows,extra->column,&c,NULL);
i;i=rxv_spin_next(c)){
if(!strcmp(val,rxv_spin_string(d=rxv_spin_this(c))))
rxv_spin_datum(pool,val,d);
}
return 1;
}
/* apr_table_do: check if parameter is empty */
static int validate_empty(void *rec,const char *key,const char *val){
if(!(val && strlen(val)))
return 0;
return 1;
}
/* apr_table_do: check if parameter is valid e-mail address */
static int validate_email(void *rec,const char *key,const char *val){
rxv_spin_ctx_t *ctx=rec;
feedback_extra_t *extra=rxv_spin_xget(ctx);
apr_pool_t *pool=rxv_spin_pool(ctx);
ap_regex_t *preg;
if(extra->preg){
preg=extra->preg;
} else{
if(!(preg=ap_pregcomp(pool,"^[^@ ]+@[^@ ]+\\.[^@ \\.]+$",
AP_REG_EXTENDED|AP_REG_NOSUB)))
return 1;
extra->preg=preg;
}
if(!val || ap_regexec(preg,val,0,NULL,0))
return 0;
return 1;
}
/* apr_table_do: check if parameter is from a list */
static int validate_lists(void *rec,const char *key,const char *val){
rxv_spin_ctx_t *ctx=rec;
apr_pool_t *pool=rxv_spin_pool(ctx);
apr_size_t i;
char *s,*l=apr_pstrcat(pool,"feedback_list_",key,NULL);
rxv_spin_data_t *list,*sgl;
rxv_spin_curs_t *lc=NULL;
if((s=rxv_spin_app_sget(ctx,l)) &&
(list=rxv_spin_parse(pool,"list",apr_pstrdup(pool,s),", \t\r\n",NULL))){
for(i=rxv_spin_first(pool,list,"list",&lc,NULL);i;i=rxv_spin_next(lc)){
sgl=rxv_spin_this(lc);
if(val && !strcmp(rxv_spin_string(sgl),val))
return 1;
}
}
return 0;
}
/* validate all named parameters, call check function for each */
static int validate(rxv_spin_ctx_t *ctx,const char *fields,
apr_table_do_callback_fn_t *check){
int rc=0;
apr_size_t i;
char *s,*p;
rxv_spin_data_t *data;
feedback_extra_t *extra=rxv_spin_xget(ctx);
apr_pool_t *pool=rxv_spin_pool(ctx);
rxv_spin_curs_t *dc=NULL;
if((s=rxv_spin_app_sget(ctx,fields)) &&
(data=rxv_spin_parse(pool,"data",apr_pstrdup(pool,s),", \t\r\n",NULL))){
for(i=rxv_spin_first(pool,data,"data",&dc,NULL);i;i=rxv_spin_next(dc)){
p=rxv_spin_string(rxv_spin_this(dc));
if(extra->params){
if(!apr_table_do(check,ctx,extra->params,p,NULL)){
rxv_spin_sset(ctx,apr_pstrcat(pool,p,"_error",NULL),empty);
rc=1;
}
}
}
}
return rc;
}
/* place all requested fields into a database */
static const char *store_fields(rxv_spin_ctx_t *ctx){
apr_size_t i;
char *conninfo,*fb,*fi,*time,*s,*feedid,*p;
const char *email;
apreq_handle_t *req=rxv_spin_req(ctx);
apreq_param_t *param;
rxv_spin_data_t *store,*result=NULL;
rxv_spin_curs_t *sc=NULL;
rxv_spin_db_t *conn;
rxv_spin_db_txn_t *txn;
apr_pool_t *pool=rxv_spin_pool(ctx);
/* do we need to store? */
if(!((s=rxv_spin_app_sget(ctx,"feedback_store")) &&
(store=rxv_spin_parse(pool,"st",apr_pstrdup(pool,s),", \t\r\n",NULL))))
return NULL;
/* important application values, parameters and current time */
if(!(param=apreq_param(req,"email")))
return "e-mail address not specified";
email=param->v.data;
if(!(conninfo=rxv_spin_app_sget(ctx,"feedback_db_connect")))
return "connect info not specified";
if(!(fb=rxv_spin_app_sget(ctx,"feedback_db_feedback")))
return "table feedback not specified";
if(!(fi=rxv_spin_app_sget(ctx,"feedback_db_feedbackitem")))
return "table feedbackitem not specified";
apr_ctime(time=apr_palloc(pool,APR_CTIME_LEN),apr_time_now());
/* connect to the database */
if(!(conn=rxv_spin_db_open(ctx,conninfo)))
return "cannot connect to the database";
/* transaction start */
if(!(txn=rxv_spin_db_start(pool,conn)))
return "cannot start transaction";
/* insert feedback */
s=apr_psprintf(pool,"insert into %s (date,email) values "
"(cast(%%s as timestamp),%%s)",fb);
if(rxv_spin_db_pquery(pool,conn,s,time,email,NULL)<=0){
rxv_spin_db_end(txn);
return "insert into feedback failed";
}
/* find out what was just inserted */
s=apr_psprintf(pool,"select feedid from %s "
"where email=%%s and date=cast(%%s as timestamp)",fb);
if(!(result=rxv_spin_db_pselect(pool,conn,s,email,time,NULL))){
rxv_spin_db_end(txn);
return "select of feedback failed";
}
if(!rxv_spin_size(result)){
rxv_spin_db_end(txn);
return "feedback not found";
}
rxv_spin_first(pool,result,"feedid",&sc,NULL);
feedid=rxv_spin_string(rxv_spin_this(sc));
/* insert feedback items */
for(i=rxv_spin_first(pool,store,"st",&sc,NULL);i;i=rxv_spin_next(sc)){
p=rxv_spin_string(rxv_spin_this(sc));
if((param=apreq_param(req,p))){
s=apr_psprintf(pool,
"insert into %s (feedid,field,content) values (%%d,%%s,%%s)",fi);
if(rxv_spin_db_pquery(pool,conn,s,feedid,p,param->v.data,NULL)<=0){
rxv_spin_db_end(txn);
return "insert into feedbackitem failed";
}
}
}
/* transaction end */
rxv_spin_db_end(txn);
return 0;
}
static apr_status_t magic_cleanup(void *cookie){
magic_close(cookie);
return APR_SUCCESS;
}
/* build the body of the e-mail message: callback function */
static const char *email_body(void **buf,int *len,void *arg){
char *line=NULL;
const apreq_param_t *param;
rxv_spin_ctx_t *ctx=arg;
feedback_extra_t *extra=rxv_spin_xget(ctx);
apr_pool_t *pool=rxv_spin_pool(ctx);
apreq_handle_t *req=rxv_spin_req(ctx);
struct{
apr_size_t index; /* where are we in the data */
apr_size_t length; /* max length of the name before value */
rxv_spin_data_t *fields; /* what fields to mail */
apr_size_t fsz; /* size of fields */
rxv_spin_data_t *names; /* names of those fields */
apr_size_t nsz; /* size of names */
rxv_spin_data_t *uploads; /* what uploaded files to mail */
apr_size_t usz; /* size of uploads */
char *boundary; /* the boundary string */
apr_bucket_brigade *brigade; /* bucket brigade we're reading */
apr_bucket *bucket; /* the current bucket */
apr_size_t blen; /* length of the buffer from the bucket */
apr_size_t offset; /* how far within the bucket we are */
char *buffer; /* contents of the bucket */
char *chunk; /* the chunk we're encoding */
char *encoded; /* encoded part of the file */
unsigned char location; /* 0: header, 1: body, 2: out */
unsigned char position; /* 0: beginning, 1: plain, 2: files, 3: end */
} *st;
/* allocate state structure */
if(!*buf)
*buf=calloc(1,sizeof(*st));
st=*buf;
/* generate boundary */
if(!st->boundary)
st->boundary=apr_psprintf(pool,"--_=_MIME.boundary.%d",rand());
/* process each member */
if(len){
/* boundary and headers of the plain message part */
if(st->position==0){
line=apr_psprintf(pool,
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n"
"--%s\r\n"
"Content-Type: text/plain\r\n\r\n",st->boundary,st->boundary);
*len=strlen(line);
st->position++;
/* plain text part of the message */
} else if(st->position==1 && st->fields &&
st->indexfields)){
rxv_spin_data_t *f=rxv_spin_entry(st->fields,"fields",st->index+1),
*n=rxv_spin_entry(st->names,"fields",st->index+1);
/* skip any non-existent parameters from the list */
while(st->indexfsz && !(param=apreq_param(req,rxv_spin_string(f)))){
st->index++;
f=rxv_spin_entry(st->fields,"fields",st->index+1),
n=rxv_spin_entry(st->names,"fields",st->index+1);
}
/* we're still within the list */
if(st->indexfsz){
/* multi line or single line output depending on the length */
if(param->v.dlen>72-st->length)
line=apr_psprintf(pool,"%*.*s:\r\n-----\r\n%s\r\n-----\r\n",
st->length,st->length,
rxv_spin_string(n),param->v.data);
else
line=apr_psprintf(pool,"%*.*s: %s\r\n",
st->length,st->length,
rxv_spin_string(n),param->v.data);
*len=strlen(line);
st->index++;
}
/* if out, go to next section */
if(st->index>=st->fsz){
st->position++;
st->index=0;
}
/* file attachments */
} else if(st->position==2 && st->uploads && st->indexusz){
rxv_spin_data_t *f=rxv_spin_entry(st->uploads,"uploads",st->index+1);
/* skip any non-existent uploads from the list */
while(st->indexusz &&
!(param=apreq_upload(extra->params,rxv_spin_string(f)))){
st->index++;
f=rxv_spin_entry(st->uploads,"uploads",st->index+1);
}
/* we're still within the list */
if(st->indexusz){
/* prepare MIME magic if we didn't before */
if(!extra->magic && (extra->magic=magic_open(MAGIC_MIME))){
apr_pool_cleanup_register(pool,extra->magic,magic_cleanup,NULL);
magic_load(extra->magic,NULL);
}
/* allocate buffers if we didn't before */
if(!st->buffer)
if(!((st->buffer=apr_palloc(pool,APR_BUCKET_BUFF_SIZE+1)) &&
(st->encoded=apr_palloc(pool,BUF_SIZE+1)) &&
(st->chunk=apr_palloc(pool,B64_SIZE+1))))
st->location=2;
/* do the header */
if(st->location==0){
/* set the first bucket */
st->brigade=param->upload;
st->bucket=APR_BRIGADE_FIRST(st->brigade);
st->offset=0;
/* we don't have the data yet, read it */
if(st->bucket!=APR_BRIGADE_SENTINEL(st->brigade) &&
apr_bucket_read(st->bucket,(const char **)&st->buffer,
&st->blen,APR_BLOCK_READ)==APR_SUCCESS){
const char *mtype;
/* if we couldn't get magic, set default MIME type */
if(!(extra->magic &&
(mtype=magic_buffer(extra->magic,st->buffer,st->blen))))
mtype="application/octet-stream";
line=apr_psprintf(pool,
"--%s\r\n"
"Content-Type: %s; name=\"%s\"\r\n"
"Content-Transfer-Encoding: base64\r\n"
"Content-Disposition: attachment; filename=\"%s\"\r\n\r\n",
st->boundary,mtype,param->v.data,param->v.data);
*len=strlen(line);
st->location++;
} else{ /* can't do anything without buffer, bail */
st->location=2;
}
/* do the body */
} else if(st->location==1){
int elen;
apr_size_t length,chunk=0;
while(chunkbucket!=APR_BRIGADE_SENTINEL(st->brigade)){
/* we need to read the bucket*/
if(!(st->offset || st->blen)){
if(apr_bucket_read(st->bucket,(const char **)&st->buffer,
&st->blen,APR_BLOCK_READ)!=APR_SUCCESS)
break;
}
/* how much do we need to copy from this bucket */
if((length=st->blen-st->offset)>B64_SIZE-chunk)
length=B64_SIZE-chunk;
/* still have more in this bucket */
if(length>0){
memcpy(st->chunk+chunk,st->buffer+st->offset,length);
st->offset+=length;
chunk+=length;
}
/* need to go to another bucket */
if(length<=0 && st->offset>=st->blen){
st->bucket=APR_BUCKET_NEXT(st->bucket);
st->offset=st->blen=0;
}
}
elen=apr_base64_encode(line=st->encoded,st->chunk,chunk);
strcpy(line+elen-1,"\r\n");
*len=strlen(line);
/* are we done? */
if(st->bucket==APR_BRIGADE_SENTINEL(st->brigade))
st->location++;
}
/* are we done with this file */
if(st->location>=2){
st->location=0;
st->index++;
}
}
/* if out, go to next section */
if(st->index>=st->usz){
st->position++;
st->index=0;
}
/* final boundary */
} else if(st->position==3){
line=apr_psprintf(pool,"--%s--\r\n",st->boundary);
*len=strlen(line);
st->position++;
}
} else{ /* initialise transient structure */
apr_size_t i,l;
rxv_spin_curs_t *c=NULL;
rxv_spin_data_t *s;
st->fields=rxv_spin_parse(pool,"fields",
apr_pstrdup(pool,rxv_spin_app_sget(ctx,"feedback_email")),
", \t\r\n",NULL);
if(st->fields)
st->fsz=rxv_spin_size(st->fields);
if(!(st->names=rxv_spin_parse(pool,"fields",
apr_pstrdup(pool,
rxv_spin_app_sget(ctx,"feedback_names")),
",\t\r\n",NULL)) ||
(st->fields && rxv_spin_size(st->names)fsz))
st->names=st->fields;
/* find maximum length of names */
if(st->names){
st->nsz=rxv_spin_size(st->names);
for(i=rxv_spin_first(pool,st->names,"fields",&c,NULL);
i;i=rxv_spin_next(c)){
s=rxv_spin_this(c);
if((l=rxv_spin_size(s))>st->length)
st->length=l;
}
}
/* uploaded files */
st->uploads=rxv_spin_parse(pool,"uploads",
apr_pstrdup(pool,rxv_spin_app_sget(ctx,"feedback_files")),
", \t\r\n",NULL);
if(st->uploads)
st->usz=rxv_spin_size(st->uploads);
}
return line;
}
static apr_status_t smtp_session_cleanup(void *session){
smtp_destroy_session(session);
return APR_SUCCESS;
}
/* e-mail all requested fields */
static const char *email_fields(rxv_spin_ctx_t *ctx){
char *server,*mailto,*cc;
const char *name,*email;
apreq_param_t *param;
smtp_session_t session=NULL;
smtp_message_t message=NULL;
apr_pool_t *pool=rxv_spin_pool(ctx);
apreq_handle_t *req=rxv_spin_req(ctx);
request_rec *r=rxv_spin_r(ctx);
/* get required parameters and application values first */
if(!(param=apreq_param(req,"email")))
return "e-mail address not specified";
email=param->v.data;
if(!(mailto=rxv_spin_app_sget(ctx,"feedback_mailto")))
return "mailto address not specified";
if(!(server=rxv_spin_app_sget(ctx,"feedback_host")))
return "server not specified";
/* descriptive name of the person */
name=((param=apreq_param(req,"name"))?param->v.data:"");
/* set up SMTP session and message */
if(!(session=smtp_create_session()))
return "session cannot be created";
apr_pool_cleanup_register(pool,session,smtp_session_cleanup,NULL);
if(!smtp_set_server(session,server))
return "cannot set server";
if(!(message=smtp_add_message(session)))
return "message cannot be created";
if(!smtp_set_reverse_path(message,email))
return "cannot set reverse path";
if(!smtp_add_recipient(message,mailto))
return "cannot add recipient";
if(!smtp_set_header(message,"From",name,email))
return "cannot set From header";
if(!smtp_set_header(message,"To","Feedback",mailto))
return "cannot set To header";
if((cc=rxv_spin_app_sget(ctx,"feedback_cc")) &&
(!strcasecmp(cc,"yes") || !strcasecmp(cc,"true") || !strcmp(cc,"1")))
if(!smtp_set_header(message,"Cc",name,email))
return "cannot set Cc header";
if(!smtp_set_header(message,"Subject",
apr_pstrcat(pool,"Feedback from ",
r->server->server_hostname,NULL)))
return "cannot set Subject header";
/* override headers cosmetics, we're not interested in errors */
smtp_set_header_option(message,"From",Hdr_OVERRIDE,1);
smtp_set_header_option(message,"To",Hdr_OVERRIDE,1);
smtp_set_header_option(message,"Cc",Hdr_OVERRIDE,1);
smtp_set_header_option(message,"Subject",Hdr_OVERRIDE,1);
/* this callback will create the body of the message */
if(!smtp_set_messagecb(message,email_body,ctx))
return "cannot set callback";
/* send */
if(!smtp_start_session(session))
return apr_psprintf(pool,"cannot start session: %s",
smtp_strerror(smtp_errno(),
apr_palloc(pool,BUF_SIZE),BUF_SIZE));
return NULL;
}
/* populate lists defined in the config file and select elements */
static void populate_lists(rxv_spin_ctx_t *ctx){
apr_size_t i;
char *s,*str;
rxv_spin_data_t *lists,*list=NULL,*rows=NULL,*sgl;
rxv_spin_curs_t *c=NULL;
apr_pool_t *pool=rxv_spin_pool(ctx);
apreq_handle_t *req=rxv_spin_req(ctx);
feedback_extra_t *extra=rxv_spin_xget(ctx);
if((s=rxv_spin_app_sget(ctx,"feedback_validate_lists")) &&
(lists=rxv_spin_parse(pool,"lists",apr_pstrdup(pool,s),", \t\r\n",NULL))){
for(i=rxv_spin_first(pool,lists,"lists",&c,NULL);i;i=rxv_spin_next(c)){
sgl=rxv_spin_this(c);
str=rxv_spin_string(sgl);
if((s=rxv_spin_app_sget(ctx,
apr_pstrcat(pool,"feedback_list_",str,NULL))) &&
(list=rxv_spin_parse(pool,str,apr_pstrdup(pool,s),",\t\r\n",list))){
apreq_param_t *param;
rows=rxv_spin_rows(pool,rows,list,
rxv_spin_null(pool,"selected",rxv_spin_size(list),NULL),NULL);
if((param=apreq_param(req,str))){
extra->column=str;
extra->marker="selected";
extra->rows=rows;
apr_table_do(list_select,ctx,extra->params,str,param->v.data,NULL);
}
rxv_spin_set(ctx,apr_pstrcat(pool,str,"_list",NULL),rows);
}
}
}
}
/* main service function */
int rxv_spin_service(rxv_spin_ctx_t *ctx){
int rc=0;
const char *error;
apr_pool_t *pool=rxv_spin_pool(ctx);
apreq_handle_t *req=rxv_spin_req(ctx);
request_rec *r=rxv_spin_r(ctx);
feedback_extra_t *extra=apr_pcalloc(pool,sizeof(*extra));
/* set extra data */
rxv_spin_xset(ctx,extra);
/* store parsed parameters and file uploads */
extra->params=apreq_params(req,pool);
/* the form has just been submitted */
if(apreq_param(req,"submit")){
/* validate empty, email and lists */
rc|=validate(ctx,"feedback_validate_empty",validate_empty);
rc|=validate(ctx,"feedback_validate_email",validate_email);
rc|=validate(ctx,"feedback_validate_lists",validate_lists);
/* we're fine: store, email */
if(!rc){
if((error=store_fields(ctx))){
ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r,
"feedback: store: %s",error);
rxv_spin_sset(ctx,"store_ERROR",empty);
}
if((error=email_fields(ctx))){
ap_log_rerror(APLOG_MARK,APLOG_ERR,APR_EGENERAL,r,
"feedback: email: %s",error);
rxv_spin_sset(ctx,"email_ERROR",empty);
}
rxv_spin_sset(ctx,"finished",empty);
return OK;
}
}
/* fresh start or validation errors, populate */
if(extra->params)
apr_table_do(ctx_set,ctx,extra->params,NULL);
/* populate and select lists */
populate_lists(ctx);
return OK;
}