/** * 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; }