string cvs_version = "$Id: realrawww.pike,v 1.14 1999/04/07 17:12:13 roby Exp $"; int thread_safe=1; #include #include //#include inherit "module"; inherit "roxenlib"; //inherit Gdbm; //------------------------------------------------------------ // GLOBALS!!!! // here is a mapping to hold roll codes mapping rollCodeMapping = ([]); int uniqCount = 0; // locks to make it thread safe #ifdef THREADS object log_lock = Thread.Mutex(); object err_lock = Thread.Mutex(); object rollcode_lock = Thread.Mutex(); object uniqCount_lock = Thread.Mutex(); object gdbm_lock = Thread.Mutex(); #endif //------------------------------------------------------------ // setup functions array register_module() { return ({ MODULE_PARSER, "Real Rawww module", ("Adds extended RealNetworks extentions to RXML -- mostly\n" " having to do with writing/reading files. Use the <realrawwwhelp>" " tag to get information on all the tags"), 0, 1 }); } // register_module void create() { defvar( "rollcodefile","/tmp/rollcodes.txt","Roll Code save file", TYPE_STRING, "Temporary file where roll codes will be" " stored so the codes mapping can be restored after a" " server restart.\n" ); defvar( "topdir","/home/rawww/spew","Top Directory for Write", TYPE_DIR, "Top level directory in the real filesystem where" " RNWRITE will write to. Note: RNWRITE cannot back up higher" " than this directory.\n" ); defvar( "gdbmdir","/home/rawww/gdbm","Top Directory for gdbm", TYPE_DIR, "Top level directory in the real filesystem where" " RNGDBM will find gdbms. Note: RNGDBM cannot back up higher" " than this directory.\n" ); defvar( "defaultlog","/home/rawww/logs/cgi.log","Default log file", TYPE_STRING, "Default file to write log items to.\n" ); defvar( "criticallog","/home/rawww/logs/pagery/cgi.critical.log","Critical log file", TYPE_STRING, "File to write critical items items to -- sysop will be paged.\n" ); defvar( "queuemailpath","/home/rawww/queuemail","Queue Mail path", TYPE_STRING, "Top-level directory for the Queue Mail; the" " *.queue subdirectories of this are where the e-mail" " files are written\n" ); defvar( "queuemailqueue","web","Queue Mail default queue", TYPE_STRING, "The subdirectory under Queue Mail path where e-mail" " content will be written for the queue mechanism to find it." ); defvar( "panictoemail", "welcome@real.com", "Panic To address", TYPE_STRING, "Panic address that the qmail tag uses if" " the page does not specify a To: in the body of the" " e-mail. Should be a system adminstrator's e-mail\n" ); defvar( "panicfromemail", "welcome@real.com", "Panic From address", TYPE_STRING, "Panic address that the qmail tag uses if" " the page does not specify a From: in the body of the" " e-mail. Should be a system adminstrator's e-mail\n" ); defvar( "panicsubjectemail", "RealNetworks", "Panic e-mail subject", TYPE_STRING, "Panic subject that the qmail tag uses if" " the page does not specify Subject: in the body of the" " e-mail. NOTE: since this is what the CUSTOMER will" " actually see, it should be something pleasant and" " generic\n" ); // NOTE: add code to run through and create all the directories // mentioned above { #ifdef THREADS mixed key; catch { key = rollcode_lock->lock(); }; #endif if ( ! rollCodeMapping ) rollCodeMapping = ([]); } } // create //------------------------------------------------------------ // here is where the work gets done //------------------------------------------------------------ // support routine -- any function that calls this defeats // caching by proxies. Use with tags that generate dynamic // content, such as rollcode void makeVolatile( object id ) { mapping newHeads = ([ "Expires": http_date(time(1)+5), "Last-Modified":http_date(time(1)-1) ]); if ( ! id->misc ) id->misc = ([]); if ( ! id->misc->moreheads ) id->misc->moreheads = ([]); id->misc->moreheads |= newHeads; // stash this away for other modules to use id->misc->specialLastModified = -1; id->misc->specialExpires = -1; // disable is-modified-since checking in http.pike // roby 1999/03/26 id->since = 0; } // makeVolatile //------------------------------------------------------------ // a wrapper around the localtime call that formats the misc fields mapping realrawww_myTime( mapping m ) { int t = time() + (m&&(int)m->add); if ( m && ( m->gmt || m->utc ) ) t += localtime(0)->timezone; mapping l = localtime(t); l->epoch = (string)t; if ( ++l->mon < 10 ) l->mon = "0" + l->mon; if ( l->mday < 10 ) l->mday = "0" + l->mday; if ( l->hour < 10 ) l->hour = "0" + l->hour; if ( l->min < 10 ) l->min = "0" + l->min; if ( l->sec < 10 ) l->sec = "0" + l->sec; return l; } // realrawww_myTime //------------------------------------------------------------ string realrawww_datestamp_help = "

<#tag# FORMAT=\"...\" [ADD=...] [GMT] >\n\n" "

Returns a datestamp.\n" "

The ADD argument specifies a number of SECONDS(!) to add\n" " to the epoch time before calculating the date.\n\n" "

GMT busts the time down to GMT time. UTC does the same thing.\n" "

The FORMAT argument follows the format for the Unix\n" " command-line \"date\" command, with the following options\n" " supported:\n\n" "

  • %H hour (00..23)\n" "
  • %M minute (00..59)\n" "
  • %S second (00..61)\n" "
  • %d day of month (01..31)\n" "
  • %m month (01..12)\n" "
  • %y last two digits of year (00..99)\n" "
  • %Y year (1970...)\n" "
  • %s epoch time -- seconds since 1970-01-01 00:00:00 (note: uses LOCAL epoch time, unless you also specify UTC)\n" "
  • %(http) http date (for cookies & such)\n" "
  • \n" "\n\n"; string realrawww_datestamp(string tag, mapping m, object id ) { string returnString = ""; if ( m->help ) returnString = replace( realrawww_datestamp_help,"#tag#",tag ); else { // this is dynamic content! No proxy cacheing! makeVolatile(id); mapping l = realrawww_myTime(m); if ( ! m->format || ! strlen(m->format) ) { returnString = (string)(l->year + 1900) + "." + (string)l->mon + "." + (string)l->mday + "-" + (string)l->hour + "." + (string)l->min + "." + (string)l->sec; } else { returnString = replace( m->format, ({ "%H","%M","%S","%d","%m","%y","%Y","%s" }), ({ (string)l->hour, (string)l->min, (string)l->sec, (string)l->mday, (string)l->mon, (string)(l->year % 100), (string)(l->year + 1900), (string)(l->epoch) }) ); // special cases; only build these if they are needed if ( search( returnString,"%(http)" ) > -1 ) returnString = replace( returnString,"%(http)",http_date(time()) ); } // if..else } // if..else return returnString; } // realrawww_datestamp //------------------------------------------------------------ // increments a counter; use as a uniqueifier int realrawww_uniq_counter() { #ifdef THREADS mixed key; catch { key = uniqCount_lock->lock(); }; #endif uniqCount = ( uniqCount + 1 ) & 16383; return uniqCount; } // realrawww_uniq_counter //------------------------------------------------------------ // write a string to the access log void realrawww_write_access( string q, object id ) { string old_not_query = id->not_query; // change the "not_query" member & log the information id->not_query = q; id->conf->log( ([ "error":200, "len":0 ]), id ); id->not_query = old_not_query; } // realrawww_write_access //------------------------------------------------------------ // write an error log entry void realrawww_log_error( int level, string p, string q, object id ) { // wrap the whole thing in a catch! mixed panic = catch { // this is just a notational thing; the p is a "fixed" // error message, the q is variable debug information q = ":" + p + ":" + q; // always strip a leading newline from the input if ( sizeof(q) > 1 && q[0] == '\n' ) q = q[1..sizeof(q)-1]; // no CRs at all & replace newlines q = replace( (q - "\r"),"\n"," " ); // write the error to the log file realrawww_write_access( "rxml-err:" + level + q,id ); // if level is 1 (critical) send out a page as well if ( level == 1 ) { #ifdef THREADS mixed key; catch { key = err_lock->lock(); }; #endif // open the file & write the string object o = Stdio.File(); if(!o->open(QUERY(criticallog),"wca" )) realrawww_write_access( "rxml-err:\t1\tcould not open critical log: " + o->errno() + " " + q,id ); else { // the critical log does get a newline at the end q += "\n"; int result = o->write(q); if ( result < sizeof(q) ) { realrawww_write_access( "rxml-err:\t1\tcould not write critical log: (" + o->errno() + ") " + q,id ); } // if o->close(); } // if..else } // if }; // catch if ( panic ) write( "PANIC PANIC PANIC -- log error exception: " + ((array)panic)[0] ); } // realrawww_logerror //------------------------------------------------------------ // support routine: crunch whitespace (for logging) // + convert all tabs and newlines into spaces // + convert strings of spaces into a single space // + substitute tab for \t and nl for \n // + suppress leading and trailing spaces around tabs and newlines string realrawww_crunch_white( string q ) { q = replace( q,"\n"," " ); q = replace( q,"\t"," " ); int safety = 1000; while ( search( q," " ) > -1 && --safety > 0 ) q = replace( q," "," " ); q = replace( q,"\\t","\t" ); q = replace( q,"\\n","\n" ); q = replace( q," \n","\n" ); q = replace( q,"\n ","\n" ); q = replace( q," \t","\t" ); q = replace( q,"\t ","\t" ); return q; } // realrawww_crunch_white //------------------------------------------------------------ string realrawww_write_help = "

    <#tag# [file=\"file name\"] [NOCRUNCH] [NEWLINE=\"x\"] [NEW] [BLOCKLOG] >...</#tag#>\n\n" "

    Writes the contents to the specified file. If no file\n" " is specified, appends to #filename#. If there isn't a newline at the\n" " end of the line, one will be added.\n" "

    The path is relative to #topdir# ... you cannot back up to\n" " a directory above this directory.\n\n" "

    Normally, whitespace is compressed and coverted thus: convert\n" " all tabs and newlines to spaces, replace strings of spaces with a single\n" " space, then convert \\n and \\t to newline and tab.\n" "

    NOCRUNCH suppresses this whitespace handling. However, newlines\n" " are still removed; use NEWLINE=\"\\n\" to preserve newlines.\n\n" "

    NEWLINE specifies a string to replace newlines with; note that\n" " unless NOCRUNCH is specified, newlines are converted to spaces.\n" " The newline replacement happens BEFORE crunching, so if you want\n" " to replace newlines with tabs, use newline=\"\\t\"\n" "

    NOTE: to leave newlines as newlines, specify newline=\"\\n\"!\n" "

    HINT: You are usually better off skipping NOCRUNCH and NEWLINE\n" " and just putting \\t and \\n specifically where you want tabs\n" " and newlines.\n\n" "

    Normally, writing appends to the specified file, creating it\n" " if it doesn't exist.\n\n" "

    NEW forces the creation of new file; it will not overwrite the\n" " file if it already exists! (creates critical error -- make sure\n" " you have a good uniquifying scheme for file names)\n\n" "

    CLOBBER overwrites an existing file, or creates it if it\n" " doesn't exist. CLOBBER supercedes new, so \"CLOBBER NEW\"\n" " is the same as \"CLOBBER\" alone.\n\n" "

    BLOCKLOG makes this tag will be mutually exclusive with\n" " any rnlogquery tags, or the normal access log logging.\n" "

    REPLACEFROM= and REPLACETO= specify a REPLACEDELIM= \n" " seperated list of replace strings. The default delimiter\n" " is # (predictably).\n\n"; string realrawww_write(string tag, mapping m, string q, object id ) { //GLOMPH string returnString = ""; string returnString = ""; if ( m->help ) returnString = replace( realrawww_write_help, ({ "#tag#","#filename#","#topdir#" }), ({ tag,QUERY(defaultlog),QUERY(topdir) }) ); else if ( m->blocklog && id->misc->rawww_log_string ) { // if blocklog is set, treat it like rnlogquery: // logging tags are mutually exclusive of each other, // regardless of whether the main page gets logged ; } else { // this is dynamic content! No proxy cacheing! makeVolatile(id); if ( m->quiet ) returnString = ""; // if no filename specified, use the default string fileName = (m->file) ? QUERY(topdir) + m->file : QUERY(defaultlog); if ( ! m->file && m->filename ) fileName = QUERY(topdir) + m->filename; // If they snuck a "../" in there to back up, just write // to default log if ( search( fileName,"../" ) > -1 ) fileName = QUERY(defaultlog); // expand anything that needs expanding; no CRs at all! string outString = parse_rxml(q,id) - "\r"; // now do some replacements -- this allows you to suppress // parsing on some elements if ( m->replacefrom && m->replaceto ) { string replacedelim = (m->replacedelim) ?m->replacedelim :"#"; outString = replace( outString,m->replacefrom/replacedelim,m->replaceto/replacedelim ); } // if // always strip a leading newline from the input if ( sizeof(outString) > 0 && outString[0] == '\n' ) outString = outString[1..sizeof(outString)-1]; // either replace or suppress raw newlines if ( m->newline ) outString = replace( outString,"\n",m->newline ); else outString -= "\n"; // crunch whitespace unless they don't want to // if not, just do substitution for \n and \t literals if ( !m->nocrunch ) outString = realrawww_crunch_white(outString); else outString = replace( replace( outString,"\\n","\n" ),"\\t","\t" ); // if nothing left, error; otherwise log the string if ( sizeof(outString) == 0 ) { realrawww_log_error( 2,"Didnt write null string",fileName, id ); } else { // make sure the last character of the string is a newline! // if ( outString[-1] != "\n" ) // outString += "\n"; // append the content string to the file; the locking // may not be necessary -- "may" being the key word #ifdef THREADS mixed key; catch { key = log_lock->lock(); }; #endif // open the file & write the string object o = Stdio.File(); string how = "wca"; if ( m->clobber ) how = "wct"; else if ( m->new ) how = "wcx"; if(!o->open(fileName,how )) { if ( !m->quiet ) returnString = ""; realrawww_log_error( 1,"Error opening",fileName + " " + o->errno(), id ); write(outString); } else { // write(outString); int result = o->write(outString); if ( result < sizeof(outString) ) { if ( !m->quiet ) returnString = ""; realrawww_log_error( 1,"Error writing",fileName + " " + o->errno(), id ); write(outString); } // if o->close(); // if blocklog is set, treat it like rnlogquery: // set a flag to say we've already logged this one if ( m->blocklog && !(m->logall) ) id->misc->rawww_log_string = !(m->logall); } // if..else } // if..else } // if..else return returnString; } // realrawww_log //------------------------------------------------------------ string realrawww_pid_help = "

    <#tag# >\n\n" "

    Returns the pid of the current server\n" " Useful for uniqueifing.\n\n"; string realrawww_pid(string tag, mapping m, object id ) { string returnString = ""; if ( m->help ) returnString = replace( realrawww_pid_help,"#tag#",tag ); else returnString = (string)getpid(); return returnString; } // realrawww_pid //------------------------------------------------------------ string realrawww_counter_help = "

    <#tag# >\n\n" "

    Returns a counter which is incremented (threadsafe) each time\n" " this tag is called. Useful for uniqueifing.\n\n"; string realrawww_counter(string tag, mapping m, object id ) { string returnString = ""; if ( m->help ) returnString = replace( realrawww_counter_help,"#tag#",tag ); else { // this is dynamic content! No proxy cacheing! makeVolatile(id); returnString = (string)realrawww_uniq_counter(); } // if..else return returnString; } // realrawww_counter //------------------------------------------------------------ string realrawww_queuemail_help = "
    <#tag# [QUEUE=\"queue\"] >content</#tag#>\n\n" "

    Sends the content into a queue directory, which is a\n" " subdirectory of #dir# . A background process watches this\n" " directory and will periodically send the content as an e-mail.\n"; string realrawww_queuemail(string tag, mapping m, string q, object id ) { string returnString = ""; if ( m->help ) returnString = replace( realrawww_queuemail_help, ({ "#tag#","#dir#" }), ({ tag,QUERY(queuemailpath) }) ); else { // this is dynamic content! No proxy cacheing! makeVolatile(id); string queue = QUERY(queuemailqueue); if ( m->queue ) queue = m->queue; // NOTE: add code to check the existence of the queue dir here mapping l = realrawww_myTime(0); int counter = realrawww_uniq_counter(); string fileName = QUERY(queuemailpath) + "/" + queue + ".queue/" + (string)l->mon + (string)l->mday + "-" + (string)l->hour + (string)l->min + "-" + (string)counter + ".go"; // munch leading whitespace! int i = 0; int length = sizeof(q); for ( i = 0 ; i < length && search( "\n\r\t ",q[i..i] ) > -1 ; i++ ) ; string sendString = parse_rxml(q[i..length-1],id); // now write the string to the queue object o = Stdio.File(); // NOTE: to avoid a race condition with the sender script, // open to create; open and write as .tmp, then move into place if ( ! sendString ) { write( "cannot send NULL email!\n" ); realrawww_log_error( 2,"cannot send NULL email",fileName, id ); returnString = ""; } else if ( ! o->open(fileName,"wcax") ) { // NOTE: DO NOT overwrite existing file! write( "Could not open file " + fileName + "\n" ); realrawww_log_error( 1,"Error opening",fileName + " " + o->errno(), id ); returnString = ""; } else { string lower = lower_case(sendString); // now that the original file has been created, close it and // write to a temporary file, then move the temp into place o->close(); if ( ! o->open(fileName + ".tmp","wcax") ) { // NOTE: DO NOT overwrite existing file! write( "Could not open file " + fileName + ".tmp" + "\n" ); realrawww_log_error( 1,"Error opening",fileName + ".tmp" + " " + o->errno(), id ); returnString = ""; } else { if ( search( lower,"subject:") < 0 ) { string addSubject = "Subject: " + QUERY(panicsubjectemail) + "\n"; sendString = addSubject + sendString; write( addSubject ); realrawww_log_error( 2,"qmail no subject",fileName + " in " + id->notquery, id ); } //if if ( search( lower,"from:") < 0 ) { string addFrom = "From: " + QUERY(panicfromemail) + "\n"; sendString = addFrom + sendString; write( addFrom ); realrawww_log_error( 2,"qmail no from",fileName + " in " + id->notquery, id ); } //if if ( search( lower,"to:") < 0 ) { string addTo = "To: " + QUERY(panictoemail) + "\nSubject: NO TO IN E-MAIL! AIEEE!\n\n"; sendString = addTo + sendString; write( addTo ); realrawww_log_error( 1,"qmail no to specified", fileName + " in " + id->notquery, id ); } //if int result = o->write(sendString); if ( result < sizeof(sendString) ) { returnString = ""; realrawww_log_error( 1,"Error writing", fileName + " " + o->errno(), id ); } // if o->close; // move the temp file into place if ( mv( fileName + ".tmp", fileName ) == 0 ) { returnString = ""; realrawww_log_error( 1,"Could not move",fileName + ".tmp" + " to " + fileName + " " + o->errno(), id ); } // if } // if..else } // if..else } // if..else return returnString; } // realrawww_queuemail //------------------------------------------------------------ // support routines: save/get a mapping mapping getMapping( string fileName, object id ) { object o = Stdio.File(); mapping m = 0; if ( ! o->open(fileName,"r") ) realrawww_log_error( 1,"Could not open cache file",(string)(o->errno()) + " " + fileName, id); else { string file = o->read(); if ( !file ) realrawww_log_error( 1,"error reading cache file",(string)(o->errno()) + " " + fileName, id); else { o->close; array top = (file + "\n\n") / "\n\n"; if ( sizeof(top) < 2 ) realrawww_log_error( 1,"bad mapping cache file -- no name/value delmiter",fileName, id); else { array names = top[0]/"\n"; array values = top[1]/"\n"; if ( sizeof(names) != sizeof(values) ) realrawww_log_error( 1,"bad mapping cache file name/value different sizes",fileName + " " + sizeof(names) + "/" + sizeof(values) , id); else m = mkmapping( names,values ); } // if..else } // if..else } // if..else return m; } // get int saveMapping( mapping m, string fileName, object id ) { int result = 0; object o = Stdio.File(); if ( ! m ) { realrawww_log_error( 1, "cannot save NULL mapping!","", id ); result = 1; } if ( ! o->open(fileName,"wct") ) { write( "Could not open file " + fileName + "\n" ); realrawww_log_error( 1,"Error opening", fileName + " " + o->errno(), id ); result = 1; } else { string outString = indices(m)*"\n" + "\n\n" + values(m)*"\n" + "\n\n"; int result = o->write( outString ); if ( result < sizeof(outString) ) { realrawww_log_error( 1,"Error writing",fileName + " " + o->errno(), id ); result = 1; } // if o->close; } // if..else return result; } // save //------------------------------------------------------------ // set roll code string realrawww_set_rollcode_help = "<#tag#>\n\n" "

    Sets rollcodes inside the persistant rollcode mapping.\n" "

    Every name/value pair in the URL is set as a rollcode;\n" " note that the \"default\" rollcode name is \"code=\"\n" "

    This is a mapping of names to an arbitrary code\n" " that can be used as a key for security purposes. #tag# allows\n" " you to set these codes dynamically through a RXML page, then\n" " use the \"rollcode\" tag to insert them into other pages for\n" " security.\n" "

    When a rollcode is set, it is immediately written to a file\n" " on disk; when Roxen starts, this file is read in,\n" " making the rollcodes persistant across shutdowns.\n\n"; string realrawww_set_rollcode(string tag, mapping m, object id ) { string returnString = ""; if ( m->help ) returnString = replace( realrawww_set_rollcode_help,"#tag#",tag ); else if ( ! id->variables || sizeof( id->variables ) == 0 ) realrawww_log_error( 2,"No variables for set rollcode","", id ); else if ( ! rollCodeMapping ) realrawww_log_error( 2,"No rollCodeMapping for set rollcode","", id ); else { // each varible represents a code that needs to be stuffed into the // mapping string code = ""; #ifdef THREADS mixed key; catch { key = rollcode_lock->lock(); }; #endif // see if we need to restore the mapping first if ( ! rollCodeMapping->code ) rollCodeMapping = getMapping( QUERY(rollcodefile),id )||([]); // now set the codes in the mapping foreach ( indices(id->variables), code ) { rollCodeMapping[code] = id->variables[code]; } // foreach // ##### save all the rollCodes to a file! if ( !saveMapping( rollCodeMapping, QUERY(rollcodefile), id ) ) returnString = "got " + indices(id->variables) * " "; else returnString = "error saving " + indices(id->variables) * " "; } // if return returnString; } // realrawww_set_roll_code //------------------------------------------------------------ // send roll code string realrawww_rollcode_help = "<#tag# [code=\"name\"] >\n\n" "

    Returns a rollcode from the persistant rollcode mapping.\n" "

    If CODE is not specified, the default rollcode is returned.\n" " Note that the default code has name=\"code\".\n\n" "

    This is a mapping of names to an arbitrary code\n" " that can be used as a key for security purposes. \"setrollcode\" allows\n" " you to set these codes dynamically through a RXML page, then\n" " use #tag# to insert them into other pages for security.\n" "

    When a rollcode is set, it is immediately written to a file\n" " on disk; when Roxen starts, this file is read in,\n" " making the rollcodes persistant across shutdowns.\n\n"; string realrawww_rollcode(string tag, mapping m, object id ) { string returnString = "(error)"; if ( m->help ) returnString = replace( realrawww_rollcode_help,"#tag#",tag ); else { // this is dynamic content! No proxy cacheing! makeVolatile(id); #ifdef THREADS mixed key; catch { key = rollcode_lock->lock(); }; #endif if ( ! rollCodeMapping->code ) rollCodeMapping = getMapping( QUERY(rollcodefile), id )||([]); if ( ! rollCodeMapping ) realrawww_log_error( 2,"could not restore mapping from file",QUERY(rollcodefile),id ); else { if ( m->code && rollCodeMapping[m->code] ) returnString = rollCodeMapping[m->code]; else if ( rollCodeMapping->code ) returnString = rollCodeMapping->code; } // if } // if return parse_rxml( returnString,id ); } // Realrawww_rollcode //------------------------------------------------------------ // break up a big procedure; this chunk just gets // the match string from the parameters mapping or // the id object static string get_dbm_match_string( mapping m, object id ) { // build the string to match against string matchString = ""; if ( m->key && strlen(m->key) ) // they gave us a literal! we're done! matchString = m->key; else if ( m->var && strlen(m->var) ) { foreach ( ( m->var / " " ) - ({""}), string varName ) { // multiple values are seperated with # if ( strlen(matchString) ) matchString += "#"; if ( id->variables[varName] ) matchString += id->variables[varName]; } // foreach } else if ( id->query ) matchString = id->query; // check to see if we need to worry about // no case or stripping whitespace if ( strlen(matchString) ) { matchString -= "\r"; if ( m->lower ) matchString = lower_case(matchString); if ( m->nowhite ) matchString = ( ( matchString - " " ) - "\t" ) - "\n"; } // if return matchString; } // get_match_string static string get_data_string( mapping m, object id ) { string dataString = ""; if ( m->storequery ) // just store the query string :) dataString = id->query; else if ( m->store && strlen(m->store) && m->store != "store" ) // if they gave us the value, we're done! dataString = m->store; else if ( m->set && strlen(m->set) ) { string delim = (m->delim && strlen(m->delim) ) ? m->delim : "\t"; foreach ( (m->set / " ") - ({""}), string varName ) { // multiple values are seperated with delim if ( strlen(dataString) ) dataString += delim; if ( id->variables[varName] ) dataString += id->variables[varName]; } // foreach } // if..else return dataString; } // get_data_string // Utility: parse a URL-encoded string of name/value pairs into a mapping mapping parse_query_string( string query, void|string keepjunk ) { mapping variables = ([]); string v, a, b; foreach(query / "&", v) if(sscanf(v, "%s=%s", a, b) == 2) { a = http_decode_string(replace(a, "+", " ")); b = http_decode_string(replace(b, "+", " ")); if(variables[ a ]) variables[ a ] += "\0" + b; else variables[ a ] = b; } else if ( keepjunk ) { if(strlen( variables[keepjunk] )) variables[keepjunk] += "&"; else variables[keepjunk] = ""; variables[keepjunk] += http_decode_string( v ); } // if..else return variables; } // parse_URL_string int fetch_gdbm_row( string value, mapping m, object id ) { if ( ! value ) // not found; done ; else { string delim = (m->delim && strlen(m->delim) ) ? m->delim : "\t"; array found = value / delim; array setVars = ({}); if ( m->set ) setVars = ( m->set / " " ) - ({""}); // see if we need to do a parse on the last column if ( sizeof(found) && ( m->parse || ! sizeof(setVars) ) ) id->variables += parse_query_string( found[sizeof(found)-1] ); // write( found * "#" + " " + (string)sizeof(setVars) + "\n" ); // ##### // now run through the columns, setting variables // note that if setVars is empty, we just fall through int colNum = 0; while ( colNum < sizeof(found) && colNum < sizeof(setVars) ) { // write( (string)colNum + " " + setVars[colNum] + "\n" ); // ##### id->variables[ setVars[colNum] ] = found[colNum]; colNum++; } // while } // if } // fetch_gdbm_row string inc_gdbm_row( string value, mapping m, object id ) { string returnString = ""; if ( value && value != "0" && (int)value == 0 ) { // doh! there is something there, but its not numeric returnString = 0; } else { // not found; set value to 1, otherwise increment the value if ( ! value ) returnString = "1"; else returnString = (string) ( (int)value + 1 ); array setVars = ({}); if ( m->set ) setVars = ( m->set / " " ) - ({""}); // now run through the columns, setting variables // note that if setVars is empty, we just fall through // for inc, they all get set to "returnString" int colNum = 0; while ( colNum < sizeof(setVars) ) { // write( (string)colNum + " " + setVars[colNum] + "\n" ); // ##### id->variables[ setVars[colNum] ] = returnString; colNum++; } // while } // if return returnString; } // inc_gdbm_row //------------------------------------------------------------ string realrawww_store_gdbm_help = "
    <#tag#\n" " FILE=filename\n" " [VAR=\"<variable name>[ <variable name>]\"]\n" " [KEY=\"string\"]\n" " [SET=\"<variable name>[ <variable name>]\"]\n" " [STORE=\"string\"]\n" " [STOREQUERY]\n" " [LOWER]\n" " [NOWHITE]\n" "

    Sets variables from a dbm; the parameters closely follow\n" " the parameters to rntable in rawww. Obviously, the difference\n" " is that here the value is written from SET or STORE instead\n" " of retrieving it.\n\n" "

    FILE= specifies the name of the gdbm file to use.\n" "

    If the gdbm lookup fails, then the values named in SET will\n" " be unchanged (and possibly unset!)\n" "

    VAR determines the name of the key to use.\n" " If more than one variable is named in VAR, then all the variables\n" " named will be concatenated with # in between them: value#value#value.\n" " (Eventually, you'll be able to set the separator.)\n\n" "

    KEY= specifies a static string to use as the lookup key; note that\n" " this parameter OVERRIDES! the VAR= parameter.\n" "

    If VAR and KEY are not named, then the *raw*, *unparsed* query string\n" " will be used to match against column one.\n\n" "

    LOWER changes the input to lowercase before making the match.\n" "

    NOWHITE strips out all whitespace (tabs, spaces, newlines)\n" " before making the match.\n" "

    The SET parameter names, in order, the variables to concatenate\n" " (with DELIM) in order to build the value to store.\n\n" "

    The DELIM parameter specifies an alternate field delimiter for the\n" " SET value(s); note that newline is still the record delimiter. Note\n" " that DELIM can be more than a single character.\n\n" "

    STORE= specifies a static string to store. STORE OVERRIDES! SET.\n" " Also, if\n STORE is blank, and SET is missing or blank, then a blank\n" " will be stored for the given key.\n\n" "

    NOTE: due to a funny implementation issue, you can't use STORE=store.\n" "

    STOREQUERY overrides both STORE and SET; it just saves the query string.\n" "\n\n"; string realrawww_store_gdbm(string tag, mapping m, object id ) { string returnString = ""; // default if ( m->help ) returnString = replace( realrawww_store_gdbm_help,"#tag#",tag ); else if ( ! m->file || ! strlen(m->file) ) { returnString = "\n\n"; } else { // this is dynamic content! No proxy cacheing! makeVolatile(id); // make sure there's a variables mapping if ( ! id->variables ) id->variables = ([]); // get the string to match on; squelch CRs (always) string matchString = get_dbm_match_string(m,id); // returnString += "matchString: >>" + matchString + "<<\n"; // ##### string fileName = QUERY(gdbmdir) + m->file; // if there's no gdbm extention, add it! A straight string // search should be sufficient for now if ( search( fileName, ".gdbm" ) < 0 ) fileName += ".gdbm"; string dataString = get_data_string( m,id ); mixed err = catch { #ifdef THREADS mixed key; key = gdbm_lock->lock(); #endif object d = Gdbm.gdbm(fileName,"rwc"); d->store(matchString,dataString); d->close(); }; if ( err ) { returnString = "\n\n"; realrawww_log_error( 2,"could not open gdbm:",fileName + " " + err[0] - "\n" + " rwc",id ); id->variables->errorstring = err[0]; } // if } // if..else return returnString; } // realrawww_store_gdbm //------------------------------------------------------------ string realrawww_inc_gdbm_help = "
    <#tag#\n" " FILE=filename\n" " [VAR=\"<variable name>[ <variable name>]\"]\n" " [KEY=\"string\"]\n" " [SET=\"<variable name>[ <variable name>]\"]\n" " [STORE=\"string\"]\n" " [STOREQUERY]\n" " [LOWER]\n" " [NOWHITE]\n" " [PARSE] >\n" "

    Reads, increments, and stores a variable from a dbm; the\n" " parameters closely follow the parameters to rntable in rawww.\n" " Obviously, the difference is that here the lookup is done\n" " from a gdbm instead of from a text table.\n\n" "

    If the lookup succeeds, then the value is incremented,\n" " THEN returned -- INC behaves like ++var. If the VAR is not\n" " found, it is set to 1 in the dbm then set as usual.\n" "

    NOTE: If the key is found, and its value is non-numeric,\n" " the dbm & the SET variables remain unchanged.\n" "

    FILE= specifies the name of the gdbm file to use.\n" "

    VAR names the variables to be concatenated (with #) to build the\n" " lookup key. If more than one variable is named in VAR, then\n" " all the variables named will be concatenated with # in between\n" " them: value#value#value.\n" " (Eventually, you'll be able to set the separator.)\n\n" "

    KEY= specifies a static string to use as the lookup key; note that\n" " this parameter OVERRIDES! the VAR= parameter.\n" "

    If VAR is not named, then the *raw*, *unparsed* query string\n" " will be used to match against column one.\n\n" "

    LOWER changes the input to lowercase before making the match.\n" "

    NOWHITE strips out all whitespace (tabs, spaces, newlines)\n" " before making the match.\n" "

    The SET parameter names, in order, the variables to be set from\n" " the matching row. If there is more than one variable named, then\n" " they are all set to the same thing -- the value after incrementing.\n" "\n\n"; string realrawww_inc_gdbm(string tag, mapping m, object id ) { string returnString = ""; // default if ( m->help ) returnString = replace( realrawww_inc_gdbm_help,"#tag#",tag ); else if ( ! m->file || ! strlen(m->file) ) { returnString = "\n\n"; } else { // this is dynamic content! No proxy cacheing! makeVolatile(id); // make sure there's a variables mapping if ( ! id->variables ) id->variables = ([]); // get the string to match on; squelch CRs (always) string matchString = get_dbm_match_string(m,id); // returnString += "matchString: >>" + matchString + "<<\n"; // ##### // build the file name; // if there's no gdbm extention, add it! A straight string // search should be sufficient for now string fileName = QUERY(gdbmdir) + m->file; if ( search( fileName, ".gdbm" ) < 0 ) fileName += ".gdbm"; mixed err = catch { #ifdef THREADS mixed key; key = gdbm_lock->lock(); #endif object d = Gdbm.gdbm(fileName,"rwc"); // retrieve, increment & store string value = d->fetch(matchString); string newValue = inc_gdbm_row( value,m,id ); if ( !newValue || !strlen(newValue) ) realrawww_log_error( 1,"could not inc value", "val=[" + value + "] new=[" + newValue + "] in " + m->file + " key=[" + matchString + "]",id ); else d->store(matchString,newValue); d->close(); }; // catch if ( err ) { returnString = "\n\n"; realrawww_log_error( 2,"could not open gdbm",fileName + " " + err[0] - "\n" + " rwc",id ); id->variables->errorstring = err[0]; } } // if..else return returnString; } // realrawww_inc_gdbm //------------------------------------------------------------ string realrawww_read_gdbm_help = "
    <#tag#\n" " FILE=filename\n" " [VAR=\"<variable name>[ <variable name>]\"]\n" " [KEY=\"string\"]\n" " [SET=\"<variable name>[ <variable name>]\"]\n" " [LOWER]\n" " [NOWHITE]\n" " [PARSE] >\n" "

    Sets variables from a dbm; the parameters closely follow\n" " the parameters to rntable in rawww. Obviously, the difference\n" " is that here the lookup is done from a gdbm instead of from\n" " a text table.\n\n" "

    FILE= specifies the name of the gdbm file to use.\n" "

    If the gdbm lookup fails, then the values named in SET will\n" " be unchanged (and possibly unset!)\n" "

    If more than one variable is named in VAR, then all the variables\n" " named will be concatenated with # in between them: value#value#value.\n" " (Eventually, you'll be able to set the separator.)\n\n" "

    KEY= specifies a static string to use as the lookup key; note that\n" " this parameter OVERRIDES! the VAR= parameter.\n" "

    The DELIM parameter specifies an alternate field delimiter;\n" " note that newline is still the record delimiter. Note that DELIM\n" " can be more than a single character.\n\n" "

    If VAR is not named, then the *raw*, *unparsed* query string\n" " will be used to match against column one.\n\n" "

    LOWER changes the input to lowercase before making the match.\n" "

    NOWHITE strips out all whitespace (tabs, spaces, newlines)\n" " before making the match.\n" "

    The SET parameter names, in order, the variables to be set from\n" " the matching row. If there are more columns than variables in a row,\n" " the extra columns are ignored. If there are more variables than columns,\n" " the extra variables are unchanged -- and possibly unset! (It's easier\n" " to check and handle unset variables than it is to check or handle\n" " blank variables in RXML.)\n\n" "

    PARSE indicates that the last column should be parsed as\n" " a string of URL-encoded name/value pairs.\n\n" "

    If SET is not specified, then PARSE is assumed.\n\n" "

    NOTE: need to add a DUMP parameter that writes the contents\n" " of the lookup table to the debug log for diagnostics.\n\n"; string realrawww_read_gdbm(string tag, mapping m, object id ) { string returnString = ""; // default if ( m->help ) returnString = replace( realrawww_read_gdbm_help,"#tag#",tag ); else if ( ! m->file || ! strlen(m->file) ) { returnString = "\n\n"; } else { // this is dynamic content! No proxy cacheing! makeVolatile(id); // make sure there's a variables mapping if ( ! id->variables ) id->variables = ([]); // get the string to match on string matchString = get_dbm_match_string(m,id); // returnString += "matchString: >>" + matchString + "<<\n"; // ##### // create the file name; // if there's no gdbm extention, add it! A straight string // search should be sufficient for now string fileName = QUERY(gdbmdir) + m->file; if ( search( fileName, ".gdbm" ) < 0 ) fileName += ".gdbm"; mixed err = catch { #ifdef THREADS mixed key; key = gdbm_lock->lock(); #endif object d = Gdbm.gdbm(fileName,"r"); // retrieve, increment & store string value = d->fetch(matchString); d->close(); fetch_gdbm_row( value,m,id ); }; // catch if ( err ) { returnString = "\n\n"; realrawww_log_error( 2,"could not open gdbm",fileName + " " + err[0] - "\n" + " r",id ); id->variables->errorstring = err[0]; } } // if..else return returnString; } // realrawww_read_gdbm //------------------------------------------------------------ // a test tag string realrawww_test(string tag, mapping m, string q, object id ) { string returnString = "test:\n"; string name = ""; returnString += "

    mapping values:\n"; foreach ( indices(m),name ) returnString += "

  • " + name + " = " + m[name] + "\n"; returnString += "

    content:\n

  • " + q + "\n"; if ( id->client ) returnString += "

    user-agent: " + id->client*"/" + "\n"; return( returnString ); } // realrawww_perpl //------------------------------------------------------------ // print all the help messages mapping query_container_callers(); mapping query_tag_callers(); string realrawww_help_help = "<#tag#>\n\n" "

    Calls all the tags and containers in this module and returns their help strings.\n\n"; string realrawww_help(string thistag, mapping m, object id ) { string allHelp = ""; if ( m->help ) allHelp = replace( realrawww_help_help,"#tag#",thistag ); else { // create the "all help" string string tag = ""; mapping f1 = query_container_callers(); mapping f2 = query_tag_callers(); foreach ( indices(f1), tag ) { allHelp += f1[tag](tag,([ "help":"" ]),"",id ) + "


    \n"; } // foreach foreach ( indices(f2), tag ) { allHelp += f2[tag](tag,([ "help":"" ]),id ) + "
    \n"; } // foreach } // if..else return( allHelp ); } // realrawww_perpl //------------------------------------------------------------ // register the functions // this step is necessary mapping query_container_callers() { return ([ "qmail":realrawww_queuemail, "rnwrite":realrawww_write, "realrawwwtest":realrawww_test ]); } // query_container_callers mapping query_tag_callers() { return ([ "setrollcode":realrawww_set_rollcode, "rollcode":realrawww_rollcode, "rndatestamp":realrawww_datestamp, "rnpid":realrawww_pid, "rncounter":realrawww_counter, "rngdbm":realrawww_read_gdbm, "rngdbmstore":realrawww_store_gdbm, "rngdbminc":realrawww_inc_gdbm, "realrawwwhelp":realrawww_help ]); } // query_tag_callers // // $Log: realrawww.pike,v $ // Revision 1.14 1999/04/07 17:12:13 roby // + in the rndate and rndatestamp functions, m->add needs to be cast to // an int all of a sudden. Not sure what changed. // // Revision 1.13 1999/03/27 01:55:19 roby // + added id->since = 0 to disable is-modified-since checking in http.pike // -- i.e. volatile files should never return a 304 // // Revision 1.12 1999/03/25 01:24:46 roby // + our encoding function now encodes : as well // + the isnotselected test strips leading & trailing whitespace first // + the isnotselected checks for choose and select at the start of the // string (only) // + added an isexpired that bounces an epoch time against the current // epoch time // + updated the self-help for some of the tags (isblank, isbad, etc.) // + added an isexpired check to ... tests variables to see if // they contain an epoch date earlier than now (i.e. if they give a time, // and we are past it) // + added a rndate tag, which is (mostly) a clone of rndatestamp, but // a) I like the syntax better -- container instead of tag // b) the default is to throw the epoch date // c) lives in rawww.pike, so we don't have to put realrawww.pike just // to get a string datestamp // + bfselect is a lot more aggressive about whitespace: a) strip leading // & trailing whitespace from the "value", around each newline, // strip whitespace & convert it into a space. // + cleaned up bfselect & bfinput handling of m ... used a multiset inside // the foreach to get rid of if..then and or's // + bfselect now allows size=auto, to automatically specify size=# of options // + bfselect ALWAYS rebuilds the options (see whitespace handling above) // + In realraww, myTime now takes a mapping instead of void|int // + myTime handles utc and epoch time // + rndatestamp allows %s epoch time and %(http) http time... but use instead // // Revision 1.11 1999/03/05 02:32:49 roby // + added "quiet" modifiers to rnwrite and sockettalk that suppress any comments // output (for sockettalk, it just meant changing "noerr" to "quiet" -- noerr is // still supported though) // + changed "log" modifier for sockettalk to allow log=send log=recv log=both ; // the behavior of just plain "log" stays the same (same as log=send) // // Revision 1.10 1999/03/03 01:07:02 roby // + changed naming convention for functions to rawww_tag_tagname to make them // easier to find // + added a rawww_get_stringlist that gets a list of names & is very tolerant; // split on any whitespace or comma // + cleaned up rnlogquery and rnlogvar somewhat // + added FULL modifier to rnlogvar // + use a multiset instead of search in the munch front back procedure // + isblank now considers any string of only whitespace blank // + beefed up the to make it functional // + added a mod10 check to veryfiy credit card #s // + added tag to replace desc="..." & make formatting easier // + fixed a 0 mapping error that happens if there's no rollcode file // + sockettalk always pre-parses the instring // // Revision 1.9 1998/12/30 03:09:40 roby // + smileys are now in color! // + beefed up rnidinfo: report what type for "tellmeall" list // + allow "misc" parameter that pushes you down a level into id->misc. This // is probably unnecessary :) // // Revision 1.8 1998/10/21 23:09:36 roby // + changed rnparse to allow multiple {} and nested {} ! Also, you can specify // the bracket strings // + added an "isdone" tag that is basically ... use it where // nesting is a problem // + created a tag which is just renamed. // + realrawww has a pid tag to help with uniquifying // + gdbm errors are written to "errorstring" // // Revision 1.7 1998/10/03 01:35:10 roby // + set a variable gdbmerr if there's an error in handling gdbms // + added a volatile files setting for the rn-customized htmlparse // + moved the rngo tag from rawww.pike to rn-customized htmlparse // + added a not parameter to rngo // + fixed the help text for rngo :) // // Revision 1.6 1998/09/16 17:25:12 roby // + go ahead and save the id->misc->special so the makeVolatile will work // across modules // // Revision 1.5 1998/09/16 17:22:06 roby // + added makeVolatile routines to rawww & realrawww // + added calls to make volatile to realraww functions (still need to do rawww) // + added rnvolatile tag to rawww // + added rn-htmlparse.pike: this customizes the standard htmlparse by carrying // along the epoch time of last modified and expieres in id->misc; update these // values when doing an insert // // Revision 1.4 1998/09/11 00:29:17 roby // + added the rn-nologging.pike module // + changed the default pagery dir from ~rawww/pagery to ~rawww/logs/pagery // + forced a little syntax on realrawww_log_error; it now expects 2 strings // instead of 1; the first is a static error message, the second is variable // diagnostic info // + wrapped realrawww_log_error in a catch statement // + changed all the calls to realrawww_log_error appropriately! // + split out the gdbm function into 3: read,inc,store // + added _log_error to the gdbm statements // + added thread locking -- only 1 thread can have any gdbm open at a time :( // this is because the call just fails and throws an exception if the file // is locked. I'll need to revisit later how to get a spin lock on those files // + wrapped the gdbm & thread stuff in a catch // + added a "plog" type to socket talk that just RXML parses the string -- // will eventually log the string too I think, but this may be redundant // with rnlogquery // + added "socketip" and "socketport" variables that can be inserted or // logged in the RXML // // Revision 1.3 1998/08/28 23:41:06 roby // + corrected the - ({}) to - ({""}) to remove blank entries from arrays // + added - ({""}) a couple of places to be safe // + removed unnecessary if varname != "" in rntable // + added a line to table help: first match counts // + dont bother with setNum/colNum ; just use setNum // + changed the set/col logic -- remove blank entries at top; don't need // an inner munch loop // + added a munchwhite container // + deleted the rnrandom function & pointed the rnrandom tag at rnshuffle; // these are close enough that there's no point having two functions // + cleaned up some of the help language in rnshuffle // + duplicates are no longer removed in rnshuffle // + added a gdbm tag that mirrors rntable in syntax // // Revision 1.2 1998/07/07 20:29:46 roby // rawww: // + minor tweaks to Makefile: use scp, added 'make banjo' // + added a NOQUERY paramter to rnlogquery -- only log variables // + log variables automatically if no query string but there are variables // (for POST mostly) // + added rnlogvar by ellenc that just logs variables -- note that I folded // this functionality into rnlogquery because it was so cool // + always strip leading & trailing whitespace from emails in isbademail // + added table lookup tag! // + added md5 hashing tag! // + minor changes to rntest tag // realrawww: // + fixed small bug in localtime; day of month does not get incremented // + changed datestamp tag to accept format= parameter // + changed some routines (read/write Mapping) to write errors to access log // instead of debug log // // Revision 1.1 1998/05/05 02:47:01 roby // + allow "variable=" in addition to "variables=" in isblank and isbademail // + moved rollcode, log, datestamp, uniqCounter, queuemail into a new module // realrawww.pike // + moved perpl,unperpl,qperp into a new module perpl.pike // + fussed with the help tag a bit -- if parameter = help, just send a // help string; otherwise do the whole list // //