table_name = $table_name; $this->sql_error_code = $sql_error_code; $this->server_error_code = $server_error_code; $this->server_error_message = $server_error_message; $message = "Failed to create table '$table_name': ($sql_error_code, $server_error_code, $server_error_message)"; parent::__construct($message); } } interface TableUpgrader { function upgrade($from_version, $to_version); } class BaseIncrementalTableUpgrader implements TableUpgrader { public $upgrade_method_names = array(); public $table_name = null; public $conn = null; function upgrade($from_version, $to_version) { // first make sure a full update is possible with what's in method_names $full_upgrade_path = true; for ($i = $from_version + 1; $i <= $to_version; $i++) { $method_name = $this->upgrade_method_names[$i]; if (!is_callable(array($this, $method_name))) { $full_upgrade_path = false; error_log(get_class($this) . "::upgrade_method_names for $i not callable: " . var_export($method_name, true)); } } if (!$full_upgrade_path) { die("couldn't upgrade the database, check error_log"); } else { $highest_version = $from_version; for ($i = $from_version + 1; $i <= $to_version; $i++) { $method_name = $this->upgrade_method_names[$i]; // XXX/TODO: this really needs trying around, not sure how to deal with upgrades that fail though, might want a downgrade function to rollback if you fail somewhere between, or just leave it in the middle state $this->$method_name(); $highest_version = $i; } return $highest_version; } } } class SimpleTableDecl { public $table_name = null; public $statements_sql = array(); // it might make sense to make this an array in the future, but for now just make it a simple string for creating the newest version public $create_table_sql = null; public $newest_tableversion = null; // int public $upgrader_classname = null; // string, this should be fully namespaced so it can be created dynamically // TODO: table dependencies variables for creating and/or for the statements? function create_table($db) { $conn = $db->conn; $conn->exec($this->create_table_sql); $err_info = $conn->errorInfo(); // the SQL errorcode for a table already existing seems to be 42S01 // but I think we don't need to check for it specifically here, // just make sure there was no error //print_r($err_info); //var_dump($err_info); if ($err_info[0] == "00000") { $db->statements["set_tableversion"]->execute(array( ":table_name" => $this->table_name, ":version" => $this->newest_tableversion )); } else if ($err_info[0] != "42S01") { $db->statements["insert_unexpected_error"]->execute(array( ":action" => "create_table($this->table_name)", ":sql_code" => $err_info[0], ":server_code" => $err_info[1], ":server_description" => $err_info[2] )); throw new TableCreationError($this->table_name, $err_info[0], $err_info[1], $err_info[2]); } } function set_statements($db) { foreach ($this->statements_sql as $name => $sql) { if (array_key_exists($name, $db->statements)) { // TODO: I should probably also raise an exception here error_log("mcoop: table_decl for $this->table_name: key $name already exists in the db's statements, not replacing"); } else { $db->statements[$name] = $db->conn->prepare($sql); }; } } function __construct($table_name, $statements_sql, $create_table_sql, $newest_tableversion, $upgrader_classname) { $this->table_name = $table_name; $this->statements_sql = $statements_sql; $this->create_table_sql = $create_table_sql; $this->newest_tableversion = $newest_tableversion; $this->upgrader_classname = $upgrader_classname; } } class SimpleDBMixin { static function load_mult_with_statement($db, $st_name, $varray, $clsname=null, $keyprop=null) { if ($clsname == null) $clsname = get_called_class(); $st = $db->statements[$st_name]; $st->execute($varray); $ret = array(); $last_obj = null; do { $last_obj = $st->fetchObject($clsname); //var_dump($st_name, $varray, $clsname, $keyprop, $st, $last_obj); //var_dump($clsname); if ($last_obj != FALSE) { if ($keyprop) { $key = $last_obj->$keyprop; // TODO: deal with duplicates $ret[$key] = $last_obj; } else { $ret[] = $last_obj; } } } while ($last_obj != FALSE); return $ret; } } // TODO: all of these classes need a get_json function so you don't serialize the entire thing (or look at how to override json // serialization for classes/objects again) class DBMember extends SimpleDBMixin { // from the members table: //public $db = null; public $userid = null; public $username = null; public $email = null; public $last_updated_table_version = null; public $full_name = null; public $validation_code = null; public $validated = null; public $full_member = null; public $argon2_password_hash = null; public $reset_password_hash = null; public $reset_requested = null; // from the shares table: //public $shares = array(); // generated in __construct public $display_name = null; function __construct() { if (isset($this->full_name) && $this->full_name) $this->display_name = $this->full_name; else $this->display_name = $this->username; } static function load_by($db, $by, $value) { // by should be either "username" or "email" here $st = $db->statements["get_member_by_$by"]; $st->execute(array($value)); $self = $st->fetchObject("\mcoop\DBMember"); if (!$self) { throw new UnknownMember($by, $value); } //$self->db = $db; return $self; } static function load_public_info($db, $userid) { $ret = DBMember::load_mult_with_statement($db, "get_public_member_info_by_userid", array(":userid" => $userid)); if (!$ret) { throw new UnknownMember("userid", $userid); } else { //$ret[0]->db = $db; return $ret[0]; } } } class TccHistoryEntry extends SimpleDBMixin { // tcc_history table values public $from_tdc_id = null; // this should probably index into a table in the task object public $to_claim_id = null; // contains this object, sometimes (the tdc could also contain them, hmm) public $action = null; public $credits = null; public $last_updated_table_version = null; static function get_history_by_claimid($db, $claimid, $clsname=null, $keyprop=null) { return TccHistoryEntry::load_mult_with_statement($db, "get_tcc_history_by_claimid", array(":claimid" => $claimid), $clsname, $keyprop); } static function get_history_by_tdc_id($db, $tdc_id, $clsname=null, $keyprop=null) { return TccHistoryEntry::load_mult_with_statement($db, "get_tcc_history_by_tdc_id", array(":tdc_id" => $tdc_id), $clsname, $keyprop); } } class SimpleTaskClaim extends SimpleDBMixin { // claims table values public $claim_id = null; public $task_id = null; public $userid = null; public $last_updated_table_version = null; public $description = null; // loaded via. userid public $member_public = null; // loaded from tcc_history, see $this->load_tcc_history public $tcc_history = null; static function get_claims_by_taskid($db, $taskid, $clsname=null, $keyprop=null) { return SimpleTaskClaim::load_mult_with_statement($db, "get_task_claims_by_taskid", array(":task_id" => $taskid), $clsname, $keyprop); } function load_member($db) { $this->member_public = DBMember::load_public_info($db, $this->userid); } function load_tcc_history($db) { $this->tcc_history = TccHistoryEntry::get_history_by_claimid($db, $this->claim_id); } function load_all($db) { $this->load_member($db); $this->load_tcc_history($db); } } class SimpleTdc extends SimpleDBMixin { // tdc table values public $tdc_id = null; public $task_id = null; public $posted_userid = null; public $posted_by_coop = null; public $total_credits = null; public $remaining_credits = null; public $last_updated_table_version = null; // loaded via. posted_userid, will be null after $this->load_member() if posted_by_coop is true public $member_public = null; // Set to either $member_public->display_name or "By the Co-Operative" // TODO: maybe use the website_name in $config with posted_by_coop public $member_name = null; static function get_tdcs_by_taskid($db, $taskid, $clsname=null, $keyprop=null) { return SimpleTdc::load_mult_with_statement($db, "get_tdcs_by_taskid", array(":task_id" => $taskid), $clsname, $keyprop); } function load_member($db) { if ($this->posted_by_coop) { $this->member_name = "By the Co-Operative"; } else { $this->member_public = DBMember::load_public_info($db, $this->posted_userid); $this->member_name = $this->member_public->display_name; } } function load_all($db) { $this->load_member($db); } } class SimpleTask extends SimpleDBMixin { // tasks table values public $taskid = null; public $admin_userid = null; public $last_updated_table_version = null; public $title = null; public $description = null; public $state = null; // loaded via. admin_userid (DBMember with userid, username and full_name filled in, see $this->load_admin -- using a join might also be a good idea but then you have to replicate the display_name code or change DBMember's construct to pass in args directly) public $member_public = null; // loaded from task_dividend_credits, see $this->load_tdcs() public $tdcs = null; // loaded from task_claims, see $this->load_claims() or $this->load_full() public $claims = null; static function get_all_simple($db, $clsname=null, $keyprop=null) { return SimpleTask::load_mult_with_statement($db, "get_all_tasks_simple", array(), $clsname, $keyprop); } static function get_all_full($db, $clsname=null, $keyprop=null) { return SimpleTask::load_mult_with_statement($db, "get_all_tasks", array(), $clsname, $keyprop); } function load_admin($db) { $this->member_public = DBMember::load_public_info($db, $this->admin_userid); } function load_claims($db, $full) { $this->claims = SimpleTaskClaim::get_claims_by_taskid($db, $this->taskid, null, "claim_id"); if ($full) { foreach ($this->claims as $k => $claim) { $claim->load_all($db); } } } function load_tdcs($db, $full) { $this->tdcs = SimpleTdc::get_tdcs_by_taskid($db, $this->taskid, null, "tdc_id"); if ($full) { foreach ($this->tdcs as $k => $tdc) { $tdc->load_all($db); } } } function load_all($db) { $this->load_admin($db); $this->load_tdcs($db, true); $this->load_claims($db, true); } } class SessionInfo { public $db = null; public $login_member = null; function __construct($db, $logout) { $this->db = $db; session_start(); if ($logout) { $this->logout(); } $this->re_init(); } function logout() { session_unset(); $this->login_member = null; } function re_init() { $this->login_member = null; if (isset($_SESSION["logged_in"]) && $_SESSION["logged_in"] && isset($_SESSION["login_username"])) { // TODO: validate login, either based on password, password reset time or possibly allow users to log out sessions explicitly (might require database session storage to make it easier) $this->login_member = DBMember::load_by($this->db, "username", $_SESSION["login_username"]); } } } ?>