/** * This is the low level error database inheritable. It handles all such * annoying things as connecting to the server and sending the queries. * There are 2 levels of functions, you can do your own queries or * leave the details to this object. * The errors database contains the following fields in the errors table: * <UL> * <LI>Id - The unique identifier for a report * <LI>DirEntryDate - The date the report was entered into the directory * (changes when forwarded) * <LI>EntryDate - The date the report was made in time() format * <LI>FixDate - The date the report was fixed or marked otherwise in time() * format * <LI>Directory - The directory the report belongs to * <LI>Filename - The filename of the object the report was made on * <LI>Category - The category the report belongs to, one of 'ROOM', 'OBJECT', * 'RITUAL', 'SPELL', 'HELP', or 'COMMAND' * <LI>Type - The type of the report, one of 'TYPO', 'BUG' or 'IDEA' * <LI>Name - The name of the object * <LI>Reporter - The name of the person who made the report * <LI>Fixer - The name of the person who fixed the report or otherwise marked * it * <LI>Status - The status of the report, one of 'OPEN', 'FIXING', * 'CONSIDERING', 'DENIED', or 'FIXED' * <LI>Report - The text of the report as typed by the reporter * <LI>Runtime - The runtime error that may or may not(!) relate to the * report * </UL> * There are is also a separate table for forwards, which has the following * fields: * <UL> * <LI>Id - The bug report id the forward belongs to * <LI>ForwardDate - The date the report was forwarder in time() format * <LI>Forwarder - The name of the person who forwarded the report * <LI>OldDirectory - The direcroy the report belonged to before forwarding * </UL> * Finally, a table for comments exists with the following fields: * <UL> * <LI>Id - The bug report id the comment belongs to * <LI>CommentDate - The date someone made a comment on the report in time() * format * <LI>Commenter - The name of the person who commented on the report * <LI>Comment - The comment about the report * </UL> * @author Turrican */ #include <db.h> #include <log.h> #include <config.h> /** @ignore yes */ private class _report { int row; string newstatus; string newdir; string newtype; int changed; } /** @ignore yes */ private class _bugs { int fd; mapping errors; int *rows; string *fieldnames; int changed; } #define FD ((class _bugs)globvars[key])->fd #define ERRORS ((class _bugs)globvars[key])->errors #define ROWS ((class _bugs)globvars[key])->rows #define FIELDNAMES ((class _bugs)globvars[key])->fieldnames #define CHANGED ((class _bugs)globvars[key])->changed private nosave mapping globvars; protected varargs mixed get_row(mixed key, int row, int nomap); /** * This method initializes some state variables and connects to the errors * database. * @param key the unique key to use for the global variables * @param user the username used to connect to the database * @param replace set to 1 if an existing key should be replaced * @return 0 for succes, an error string for failure * @see save_changes() * @see finish_errors() */ protected varargs string init_errors(mixed key, string user, int replace) { string ret; if( !mappingp(globvars) ) { globvars = ([ ]); } else { if( !( replace || undefinedp(globvars[key]) ) ) return "key already in use"; if( globvars[key] ) catch( db_close(FD) ); } globvars[key] = new(class _bugs); if( ( ret = catch( FD = db_connect("localhost", CONFIG_DB_ERRORS, CONFIG_DB_USER ) ) ) ) { map_delete(globvars, key); return ret; } return 0; } /* init_errors() */ /** * This method ends access to the database. It doesn't save any pending * changes. * @param key the unique key for the global variables * @return 0 for succes, an error string for failure * @see init_errors() * @see save_changes() */ protected string finish_errors(mixed key) { string ret; TCRE("shaydz",sprintf("key:%O",key)); if(!key) return "no key"; if (!classp(globvars[key])) { return "unknown key"; } ret = catch(db_close(FD)); map_delete(globvars, key); return ret; } /* finish_errors() */ /** * This method provides direct SQL access to the errors database. You * have to give a valid SQL query. It returns the number of rows the * query returned, which can be fetched using get_row(). For a higher * level of access to the database, use get_fields(). * @param key the unique key for the global variables * @param query the SQL query to execute * @return the number of rows or an error string * @see get_row() * @see get_fields() */ protected varargs mixed sql_query(mixed key, string query, mixed *args ...) { string error; mixed ret; ERRORS = 0; ROWS = 0; FIELDNAMES = 0; error = catch(ret = db_exec(FD, query, args ...)); if (error) { return error; } if (stringp(ret)) { return ret; } if (ret) { ROWS = allocate(ret + 1, (: $1 :)); } return ret; } /* sql_query */ /** * This method fetches the specified fields from the database. You should * not call this too often as this is a very expensive operation. Typically * it's called once, at the beginning of your object. * @param key the unique key for the global variables * @param directory the directory to get the reports from * @param fields a comma separated list of fields you wish to fetch * @return the number of reports that were found or an error string * @see get_bug() * @see get_forwards() * @see get_comments() */ protected varargs mixed get_fields(mixed key, string directory, string fields, string type) { string query, temp; mixed ret; if (!strlen(type)) { type = "OPEN"; } ERRORS = 0; ROWS = 0; FIELDNAMES = 0; fields += ", Directory"; query = sprintf("SELECT %s FROM errors WHERE Directory = '%s' AND " "Status = '%s' ORDER BY DirEntryDate;", fields, directory, type); temp = catch(ret = db_exec(FD, query)); if (temp) { return temp; } if (stringp(ret)) { return ret; } if (ret) { ROWS = allocate(ret + 1, (: $1 :)); } return ret; } /* get_fields() */ /** * This method gets the forwarding info for a report in the database. * The info is given in a mapping indexed by column name where the * values are arrays of the values for the columns, ordered by * ForwardDate. * @param key the unique key for the global variables * @param row the row number to get the forwards for * @return a mapping with the forwards or an error string * @see get_fields() * @see get_comments() */ protected mixed get_forwards(mixed key, int row) { string query, err; mixed ret; mapping forwards; int fd; ret = get_row( key, row ); if( stringp(ret) ) return ret; err = catch( fd = db_connect("localhost", CONFIG_DB_ERRORS, CONFIG_DB_USER ) ); if( err ) return err; query = sprintf("SELECT ForwardDate, Forwarder, OldDirectory FROM forwards " "WHERE Id = %d ORDER BY ForwardDate;", ret["Id"]); if( err = catch( ret = db_exec( fd, query ) ) ) { catch(db_close(fd)); return err; } if( stringp(ret) ) { catch(db_close(fd)); return ret; } if( ret ) { err = catch { int *dates, i; string *forwarders, *dirs; mixed *vals; dates = allocate(ret); forwarders = allocate(ret); dirs = allocate(ret); for (i = 0; i < ret; i++) { vals = db_fetch(fd, i + 1); dates[i] = vals[0]; forwarders[i] = vals[1]; dirs[i] = vals[2]; } forwards = allocate_mapping(3); forwards["ForwardDate"] = dates; forwards["Forwarder"] = forwarders; forwards["OldDirectory"] = dirs; }; if( err ) { catch(db_close(fd)); return err; } } catch(db_close(fd)); return forwards; } /* get_forwards() */ /** * This method gets the comments about a report in the database. * The info is given in a mapping indexed by column name where the * values are arrays of the values for the columns, ordered by * CommentDate. * @param key the unique key for the global variables * @param row the row number to get the comments for * @return a mapping with the comments or an error string * @see get_fields() * @see get_forwards() */ protected mixed get_comments(mixed key, int row) { string query, err; mixed ret; mapping comments; int fd; ret = get_row(key, row); if( stringp(ret) ) return ret; err = catch( fd = db_connect("localhost", CONFIG_DB_ERRORS, CONFIG_DB_USER ) ); if( err ) return err; query = sprintf("SELECT CommentDate, Commenter, Comment FROM comments " "WHERE Id = %d ORDER BY CommentDate;", ret["Id"]); if( err = catch( ret = db_exec( fd, query ) ) ) { catch(db_close(fd)); return err; } if( stringp(ret) ) { catch(db_close(fd)); return ret; } if( ret ) { err = catch { int *dates, i; string *commenters, *texts; mixed *vals; dates = allocate(ret); commenters = allocate(ret); texts = allocate(ret); for (i = 0; i < ret; i++) { vals = db_fetch(fd, i + 1); dates[i] = vals[0]; commenters[i] = vals[1]; texts[i] = vals[2]; } comments = allocate_mapping(3); comments["CommentDate"] = dates; comments["Commenter"] = commenters; comments["Comment"] = texts; }; if( err ) { catch(db_close(fd)); return err; } } catch(db_close(fd)); return comments; } /* get_comments() */ /** * This method returns a row of information from the database. Note that * you get the original, not a copy, so any changes you make to the row * will also be reflected in a later get_row(). If you don't want this, * just make a copy(). The row consists of a mapping with the column names * being the keys. * @param key the unique key for the global variables * @param number the row number of the bug report * @param nomap set to 1 if the real row number is given * @return a row from the database or an error string * @see get_fields() * @see efun::copy() */ protected varargs mixed get_row(mixed key, int row, int nomap) { mixed *res; string ret, *keys; mapping rowvals; if (!arrayp(FIELDNAMES)) { /* Fetch the field names. */ ret = catch(res = db_fetch(FD, 0)); if (ret) return ret; keys = res; FIELDNAMES = res; } else { keys = FIELDNAMES; } if (!nomap) { row = ROWS[row]; } /* Get the actual values of the row. */ ret = catch(res = db_fetch(FD, row)); if (ret) return ret; /* Put them in a mapping. */ if (sizeof(keys) != sizeof(res)) { return "Database engine lost results(?)\n"; } rowvals = allocate_mapping(keys, res); return rowvals; } /* get_row() */ /** * This method changes the status of a bug report. In the old system * it would delete the bug. * @param key the unique key for the global variables * @param row the row number to change the status of * @param status the new status of the report * @return 1 for succes, 0 for failure */ protected int set_status(mixed key, int row, string status) { int realrow = ROWS[row]; /* Currently, this can't fail because of missing commit/rollback facilities in MySQL. We only actually save this later on. */ if (!mappingp(ERRORS)) ERRORS = ([ ]); if (!classp(ERRORS[realrow])) { ERRORS[realrow] = new(class _report, row : realrow); } ((class _report)ERRORS[realrow])->newstatus = status; ((class _report)ERRORS[realrow])->changed = 1; ROWS = delete(ROWS, row, 1); CHANGED = 1; return 1; } /* set_status() */ /** * This method changes the type of a bug report. * Possible types are 'IDEA', 'BUG' and 'TYPO'. * @param key the unique key for the global variables * @param row the row number to change the type of * @param type the new type of the report * @return 1 for succes, 0 for failure */ protected int set_type(mixed key, int row, string type) { /* Currently, this can't fail because of missing commit/rollback facilities in MySQL. We only actually save this later on. */ if (!mappingp(ERRORS)) ERRORS = ([ ]); row = ROWS[row]; if (!classp(ERRORS[row])) { ERRORS[row] = new(class _report, row : row); } ((class _report)ERRORS[row])->newtype = type; ((class _report)ERRORS[row])->changed = 1; CHANGED = 1; return 1; } /* set_type() */ /** * This method forwards the bug report to a different directory. * @param key the unique key for the global variables * @param row the row number of the bug to forward * @param directory the name of the new directory * @return 1 for succes, 0 for failure */ protected int forward_bug(mixed key, int row, string directory) { int realrow = ROWS[row]; /* Currently, this can't fail because of missing commit/rollback facilities in MySQL. We only actually save this later on. */ if (!mappingp(ERRORS)) ERRORS = ([ ]); if (!classp(ERRORS[realrow])) { ERRORS[realrow] = new(class _report, row : realrow); } ((class _report)ERRORS[realrow])->newdir = directory; ((class _report)ERRORS[realrow])->changed = 1; ROWS = delete(ROWS, row, 1); CHANGED = 1; return 1; } /* forward_bug() */ /** * This method stores a comment about the report. * Unlike most other methods, this is immediately saved in the * comments table! * @param key the unique key for the global variables * @param row the row number of the bug to forward * @param who the name of the commenter * @param comment the text of the comment * @return 0 for succes, an error string for failure */ protected string comment_bug(mixed key, int row, string who, string comment) { string query, err; int fd; mixed ret; ret = get_row(key, row); if( stringp(ret) ) return ret; err = catch( fd = db_connect("localhost", CONFIG_DB_ERRORS, CONFIG_DB_USER ) ); if( err ) return err; comment = db_escape(comment); query = sprintf("INSERT LOW_PRIORITY INTO comments VALUES " "(%d, %d, '%s', '%s');", ret["Id"], time(), who, comment); err = catch( ret = db_exec( fd, query ) ); if( stringp(ret) ) err = ret; catch( db_close(fd) ); return err; } /* comment_bug() */ /** @ignore yes */ private string save_status(mixed key, int *ids, string user, string status) { string query, ret, err; mixed res; ret = ""; if( sizeof(ids) ) { if( sizeof(ids) == 1 ) { query = sprintf("UPDATE LOW_PRIORITY errors SET Status = '%s', " "FixDate = %d, Fixer = '%s' WHERE Id = %d;", status, time(), user, ids[0]); } else { query = sprintf("UPDATE LOW_PRIORITY errors SET Status = '%s', " "FixDate = %d, Fixer = '%s' WHERE Id IN (%s);", status, time(), user, implode(ids, (: "" + $1 + ", " + $2 :))); } if( err = catch( res = db_exec( FD, query ) ) ) ret += err; if( stringp(res) ) ret += res; } return ret; } /* save_status() */ /** * This method saves any pending changes into the database. * @param key the unique key for the global variables * @param user the name of the person who made the changes * @return 0 for succes, an error string for failure * @see finish_errors() * @see init_errors() */ protected string save_changes(mixed key, string user) { class _report *reports, report; string query, *queries, ret, err; int *ids_fixed, *ids_denied, *ids_fixing, *ids_considering, ftime; mixed res, row; if (!CHANGED) { return 0; } reports = filter(values(ERRORS), (: ((class _report)$1)->changed :)); ids_fixed = ids_denied = ids_fixing = ids_considering = queries = ({ }); ret = ""; foreach (report in reports) { row = get_row(key, report->row, 1); if (stringp(row)) { ret += row; continue; } switch (report->newstatus) { case "FIXED": ids_fixed += ({ row["Id"] }); break; case "DENIED": ids_denied += ({ row["Id"] }); break; case "FIXING": ids_fixing += ({ row["Id"] }); break; case "CONSIDERING": ids_considering += ({ row["Id"] }); break; case 0: case "": if (report->newdir) { ftime = time(); query = sprintf("UPDATE LOW_PRIORITY errors SET Directory = '%s', " "DirEntryDate = %d WHERE Id = %d;", report->newdir, ftime, row["Id"]); queries += ({ query }); query = sprintf("INSERT LOW_PRIORITY INTO forwards VALUES " "(%d, %d, '%s', '%s');", row["Id"], ftime, user, row["Directory"]); queries += ({ query }); } if (report->newtype) { query = sprintf("UPDATE LOW_PRIORITY errors SET Type = '%s' " "WHERE Id = %d;", report->newtype, row["Id"]); queries += ({ query }); } break; default: ret += sprintf("Unknown report status: %s\n", report->newstatus); break; } } ret += save_status( key, ids_fixed, user, "FIXED"); ret += save_status( key, ids_denied, user, "DENIED"); ret += save_status( key, ids_fixing, user, "FIXING"); ret += save_status( key, ids_considering, user, "CONSIDERING"); foreach( query in queries ) { if( err = catch( db_exec( FD, query ) ) ) ret += err; if( stringp(res) ) ret += res; } if( ret == "") ret = 0; return ret; } /* save_changes() */