summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyle McFarland <tfkyle@gmail.com>2018-01-31 00:51:07 -0600
committerKyle McFarland <tfkyle@gmail.com>2018-01-31 00:51:07 -0600
commit61d1aa04d8d44b17bfe6dace90088669fc6c3df8 (patch)
tree7ede15c880e4c41a18cded46fe6d03fb2dc4543b
downloadmcoop-master.zip
mcoop-master.tar.gz
mcoop-master.tar.bz2
Initial importHEADmaster
* Registration system's almost done * Just part way through implementing tasks So not much done yet, but it's a start.
-rw-r--r--.gitignore6
-rw-r--r--bower.json23
-rw-r--r--bylaws.php6
-rw-r--r--common/config.php.example34
-rw-r--r--common/config_cls.php26
-rw-r--r--common/db.php310
-rw-r--r--common/db_classes.php346
-rw-r--r--common/tables/dividend_credits.php36
-rw-r--r--common/tables/members.php47
-rw-r--r--common/tables/messages.php5
-rw-r--r--common/tables/task_claims.php32
-rw-r--r--common/tables/task_dividend_credits.php32
-rw-r--r--common/tables/tasks.php45
-rw-r--r--common/tables/tcc_history.php31
-rw-r--r--composer.json7
-rw-r--r--css/mcoop.css18
-rw-r--r--index.php14
-rw-r--r--js/tasks.js46
-rw-r--r--login.php32
-rw-r--r--logout.php15
-rw-r--r--profile.php115
-rw-r--r--register.php57
-rw-r--r--tasks.php13
-rw-r--r--tasks_api.php28
-rw-r--r--tmpl/base.tmpl64
-rw-r--r--tmpl/bylaws.tmpl0
-rw-r--r--tmpl/index.tmpl19
-rw-r--r--tmpl/login.tmpl18
-rw-r--r--tmpl/logout.tmpl9
-rw-r--r--tmpl/profile.tmpl24
-rw-r--r--tmpl/register.tmpl30
-rw-r--r--tmpl/tasks.tmpl29
-rw-r--r--tmpl/validate.tmpl19
-rw-r--r--tmpl/validation_email.tmpl9
-rw-r--r--todo.txt25
-rw-r--r--validate.php40
36 files changed, 1610 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a90e814
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+bower_components
+vendor
+setup_bower.sh
+composer.lock
+cache
+common/config.php
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..95fb3bc
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,23 @@
+{
+ "name": "mcoop",
+ "authors": [
+ "Kyle McFarland <tfkyle@gmail.com>"
+ ],
+ "description": "mcoop is a simple Co-Op management webapp",
+ "main": "index.html",
+ "license": "Apache-2.0",
+ "homepage": "http://wiki.gpio.ca/wiki/mcoop",
+ "private": true,
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ "bootstrap": "^4.0.0",
+ "tether": "^1.4.3",
+ "jquery": "^3.3.1"
+ }
+}
diff --git a/bylaws.php b/bylaws.php
new file mode 100644
index 0000000..9df06a9
--- /dev/null
+++ b/bylaws.php
@@ -0,0 +1,6 @@
+<?php
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+
+?>
diff --git a/common/config.php.example b/common/config.php.example
new file mode 100644
index 0000000..14cb84b
--- /dev/null
+++ b/common/config.php.example
@@ -0,0 +1,34 @@
+<?php
+namespace mcoop;
+/* Copy this to config.php and edit the $config declaraction below
+ appropriately */
+require_once("config_cls.php");
+require_once("db.php");
+require_once("db_classes.php");
+require_once("vendor/autoload.php");
+
+$config = new Config(
+ "mysql:host=localhost;dbname=mcoop",
+ "[username]",
+ "[password]",
+ "[recaptcha_site_key]",
+ "[recaptcha_secret_key]",
+ "Email Name <email@address>",
+ "[http://mcoop.example.com/]",
+ "[Site Name]"
+);
+
+$db = new Database($config);
+
+if (!isset($logout))
+ $logout = false;
+
+$sess_info = new SessionInfo($db, $logout);
+
+$twig_loader = new \Twig_Loader_Filesystem("./tmpl/");
+$twig = new \Twig_Environment($twig_loader, array(
+ "cache" => "./cache/",
+ "auto_reload" => true
+));
+
+?>
diff --git a/common/config_cls.php b/common/config_cls.php
new file mode 100644
index 0000000..b4174be
--- /dev/null
+++ b/common/config_cls.php
@@ -0,0 +1,26 @@
+<?php
+namespace mcoop;
+
+class Config {
+ public $pdo_connstring = null;
+ public $db_username = null;
+ public $db_password = null;
+ public $recaptcha_sitekey = null;
+ public $recaptcha_secret = null;
+ public $email_from_address = null;
+ public $webapp_base_uri = null;
+ public $website_name = null;
+
+ function __construct($pdo_connstring, $db_uname, $db_passwd, $recaptcha_sitekey, $recaptcha_sec, $email_from_addr, $webapp_base_uri, $website_name) {
+ $this->pdo_connstring = $pdo_connstring;
+ $this->db_username = $db_uname;
+ $this->db_password = $db_passwd;
+ $this->recaptcha_sitekey = $recaptcha_sitekey;
+ $this->recaptcha_secret = $recaptcha_sec;
+ $this->email_from_address = $email_from_addr;
+ $this->webapp_base_uri = $webapp_base_uri;
+ $this->website_name = $website_name;
+ }
+}
+
+?>
diff --git a/common/db.php b/common/db.php
new file mode 100644
index 0000000..71dbf81
--- /dev/null
+++ b/common/db.php
@@ -0,0 +1,310 @@
+<?php
+namespace mcoop;
+require_once("config_cls.php");
+require_once("vendor/busybee/urljoin/src/urljoin.php");
+require_once("db_classes.php");
+require_once("tables/members.php");
+require_once("tables/dividend_credits.php");
+require_once("tables/tasks.php");
+require_once("tables/task_claims.php");
+require_once("tables/task_dividend_credits.php");
+require_once("tables/tcc_history.php");
+
+// TODO: move this into the config object, or auto add to it in each decl file
+$table_decls = array(
+ "members" => $members_table_decl,
+ "dividend_credits" => $dividend_credits_table_decl,
+ "tasks" => $tasks_table_decl,
+ "task_claims" => $task_claims_table_decl,
+ "task_dividend_credits" => $task_dividend_credits_table_decl,
+ "tcc_history" => $tcc_history_table_decl
+);
+
+class UnknownMember extends \Exception {
+ public $searched = null;
+ public $value = null;
+
+ function __construct($searched, $value) {
+ $this->searched = $searched;
+ $this->value = $value;
+
+ $message = "Unable to find a member with $searched = $value";
+ parent::__construct($message);
+ }
+}
+
+class RegistrationError extends \Exception {
+ public $invalid_part = null;
+ public $reason = null;
+
+ function __construct($invalid_part, $reason) {
+ $this->invalid_part = $invalid_part;
+ $this->reason = $reason;
+
+ $message = "Registration Error ($invalid_part): $reason";
+ parent::__construct($message);
+ }
+}
+
+class LoginError extends \Exception {}
+
+class Database {
+ public $config = null;
+ public $conn = null;
+ public $target_tableversions = array(
+ "unexpected_errors" => 1,
+ );
+ public $statements = array();
+ public $upgraders = array();
+ public $table_decls = array();
+
+ function __construct($config) {
+ $this->config = $config;
+ global $table_decls;
+ $this->table_decls = $table_decls;
+ $this->connect();
+ }
+
+ function connect() {
+ // XXX/TODO: the exceptions from this need to be caught, see the warning on http://php.net/manual/en/pdo.connections.php
+ $this->conn = new \PDO(
+ $this->config->pdo_connstring,
+ $this->config->db_username,
+ $this->config->db_password,
+ array(\PDO::ATTR_PERSISTENT => true)
+ );
+ $this->create_tableversions_table();
+ $this->create_unexpected_errors_table();
+ // TODO: make table creation optional for everything except tableversions and unexpected_errors
+ // (you can change the $create argument to setup_table_decls to false for that now)
+ $this->setup_table_decls();
+ $this->auto_upgrade_all();
+ //$this->create_members_table();
+ //$this->upgraders["members"] = new MembersUpgrader($this->conn);
+ //$this->auto_upgrade_all();
+ //var_dump($this->conn);
+ }
+
+ function setup_table_decls($create=true) {
+ $conn = $this->conn;
+ foreach ($this->table_decls as $tname => $td) {
+ if ($create) {
+ $td->create_table($this);
+ }
+ $td->set_statements($this);
+ $clsname = $td->upgrader_classname;
+ $this->upgraders[$tname] = new $clsname($conn);
+ }
+ }
+
+ function get_cur_table_version($table_name) {
+ $this->statements["get_tableversion"]->execute(array(":name" => $table_name));
+ $x = $this->statements["get_tableversion"]->fetchAll(\PDO::FETCH_COLUMN);
+ $ver = (int)$x[0];
+ return $ver;
+ }
+
+ function auto_upgrade($upgrader) {
+ // This shouldn't be called until at the very least after create_tableversions_table, since it adds set_tableversion to statements
+ $table_name = $upgrader->table_name;
+ $old_ver = $this->get_cur_table_version($table_name);
+ $new_ver = $this->table_decls[$table_name]->newest_tableversion;
+ if ($old_ver < $new_ver) {
+ error_log("upgrading $table_name from $old_ver to $new_ver");
+ // XXX/TODO: this is assuming it doesn't error, going to get really ugly if it does
+ $upgraded_to = $upgrader->upgrade($old_ver, $new_ver);
+ $this->statements["set_tableversion"]->execute(array(":table_name" => $table_name, ":version" => $upgraded_to));
+ //print_r($upgraded_to);
+ return $upgraded_to;
+ }
+ }
+
+ function auto_upgrade_all() {
+ foreach ($this->upgraders as $table_name => $upgrader) {
+ $this->auto_upgrade($upgrader);
+ }
+ }
+
+ function create_tableversions_table() {
+ $conn = $this->conn;
+ $conn->exec("CREATE TABLE IF NOT EXISTS `table_versions` (`name` TEXT CHARACTER SET utf8 NOT NULL, `version` INT, PRIMARY KEY (`name`(1024)));");
+ $this->statements["set_tableversion"] = $conn->prepare("INSERT INTO table_versions VALUES (:table_name, :version) on duplicate key UPDATE version=:version");
+ $this->statements["get_tableversion"] = $conn->prepare("SELECT version FROM table_versions WHERE name = :name");
+ }
+
+ function create_unexpected_errors_table() {
+ // TODO: this table needs a datetime field so you know when the error happened
+ $conn = $this->conn;
+ $conn->exec("CREATE TABLE `unexpected_errors` (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `action` TEXT CHARACTER SET utf8 NOT NULL, `error_sql_code` CHAR(5) CHARACTER SET ascii NOT NULL, `error_server_code` INT, `error_server_description` TEXT CHARACTER SET utf8);");
+ $err_info = $conn->errorInfo();
+ //var_dump($err_info);
+ if ($err_info[0] == "00000") {
+ $this->statements["set_tableversion"]->execute(array(
+ ":table_name" => "unexpected_errors",
+ ":version" => $this->target_tableversions["unexpected_errors"]
+ ));
+ }
+ $this->statements["insert_unexpected_error"] = $conn->prepare("INSERT INTO unexpected_errors (action, error_sql_code, error_server_code, error_server_description) VALUES (:action, :sql_code, :server_code, :server_description)");
+ }
+
+ function create_members_table() {
+ $conn = $this->conn;
+ $conn->exec(
+ "CREATE TABLE `members` (
+`userid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+`username` TEXT CHARACTER SET utf8 NOT NULL,
+`email` TEXT CHARACTER SET utf8 NOT NULL,
+`last_updated_table_version` INT NOT NULL,
+`full_name` TEXT CHARACTER SET utf8,
+`validation_code` CHAR(64) CHARACTER SET ascii,
+`validated` BOOL DEFAULT false,
+`full_member` BOOL DEFAULT false,
+`argon2_password_hash` VARCHAR(256) CHARACTER SET ascii,
+`reset_password_hash` VARCHAR(256) CHARACTER SET ascii,
+`reset_requested` BOOL DEFAULT false,
+UNIQUE KEY (`username`(256)),
+UNIQUE KEY (`email`(256))
+);");
+ $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") {
+ $this->statements["set_tableversion"]->execute(array(
+ ":table_name" => "members",
+ ":version" => $this->target_tableversions["members"]
+ ));
+ } else if ($err_info[0] != "42S01") {
+ $this->statements["insert_unexpected_error"]->execute(array(
+ ":action" => "create_members_table",
+ ":sql_code" => $err_info[0],
+ ":server_code" => $err_info[1],
+ ":server_description" => $err_info[2]
+ ));
+ }
+ $this->statements["register_member"] = $conn->prepare("INSERT INTO members (username, email, last_updated_table_version, full_name, argon2_password_hash) VALUES (:username, :email, :version, :full_name, :argon2_phash)");
+ $this->statements["get_member_passhash_by_username"] = $conn->prepare("SELECT argon2_password_hash FROM members WHERE username = :username");
+ $this->statements["get_member_by_username"] = $conn->prepare("SELECT * FROM members WHERE username = ?");
+ $this->statements["get_member_by_email"] = $conn->prepare("SELECT * FROM members WHERE email = ?");
+ $this->statements["get_members_by_uname_or_email"] = $conn->prepare("SELECT * FROM members WHERE username = :username OR email = :email");
+ }
+
+ function validate_username($username) {
+ if (!preg_match("/^[a-zA-Z0-9]{3,}$/", $username)) {
+ throw new RegistrationError("username", "Invalid username (must be at least 3 characters long, only english alphanumeric characters are allowed");
+ }
+ $conn = $this->conn;
+ $username_check = $conn->prepare("SELECT * FROM members WHERE username = :username");
+ $username_check->execute(array(":username" => $username));
+ $x = $username_check->fetchAll(\PDO::FETCH_COLUMN);
+ if ($x) {
+ throw new RegistrationError("username", "Username already registered");
+ }
+ return $username;
+ }
+
+ function validate_email($email) {
+ $filt_email = filter_var($email, FILTER_VALIDATE_EMAIL);
+ if ($filt_email != $email) {
+ throw new RegistrationError("email", "Doesn't validate as a valid email address");
+ }
+ $conn = $this->conn;
+ $email_check = $conn->prepare("SELECT * FROM members where email = :email");
+ $email_check->execute(array(":email" => $filt_email));
+ $x = $email_check->fetchAll(\PDO::FETCH_COLUMN);
+ if ($x) {
+ throw new RegistrationError("email", "An account with that email address already exists, you can <a href=\"/reset_password.php\">Reset your password</a> if you need a new password");
+ }
+ return $filt_email;
+ }
+
+ function validate_fullname($full_name) {
+ $filt_fname = filter_var($full_name, FILTER_SANITIZE_STRING, array(
+ "flags" => FILTER_FLAG_NO_ENCODE_QUOTES | FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH | FILTER_FLAG_STRIP_BACKTICK
+ ));
+ return $filt_fname;
+ }
+
+ // TODO: reset password functionality that sends a randomly generated password to your email address
+ function register($username, $email, $password, $full_name, $validate_email=false, $email_twig_env=null) {
+ $filt_email = $this->validate_email($email);
+ $filt_username = $this->validate_username($username);
+ $filt_fname = $this->validate_fullname($full_name);
+ $statements = $this->statements;
+ $conn = $this->conn;
+ $pass_hash = password_hash($password, PASSWORD_ARGON2I);
+ $conn->beginTransaction();
+ $success = $statements["register_member"]->execute(array(
+ ":username" => $username,
+ ":email" => $filt_email,
+ ":version" => $this->table_decls["members"]->newest_tableversion,
+ ":full_name" => $filt_fname,
+ ":argon2_phash" => $pass_hash
+ ));
+ // TODO: check that it was actually successful
+ if (!$success) {
+ $einfo = $statements["register_member"]->errorInfo();
+ error_log("mcoop member registration failed inserting into the database: " . var_export($einfo, true) . " ($username, $email, $password, $pass_hash, $full_name)");
+ throw new RegistrationError("database", "registration failed internally, contact the admin");
+ }
+ if ($validate_email) {
+ $this->send_validation_email($username, $filt_email, $email_twig_env);
+ }
+ $conn->commit();
+ // TODO: setup the login session here too, not sure if session_start should be called in the script setup code actually instead of just here (and just set the variable in $_SESSION here)
+ //session_start();
+ $_SESSION["logged_in"] = true;
+ $_SESSION["login_username"] = $username;
+ // TODO: include login time in here and other things so you can invalidate sessions/log people out if a password reset gets requested or anything else
+ //session_write_close();
+ }
+
+ function send_validation_email($username, $email, $email_twig_env) {
+ // generate a validation code, just use some random bytes, hash them and use the hexdigest for now
+ $rbytes = random_bytes(64);
+ $h = hash("sha256", $rbytes, false);
+ $conn = $this->conn;
+ $stmt = $conn->prepare("UPDATE members SET validation_code = :vcode WHERE username = :username AND email = :email");
+ $success = $stmt->execute(array(
+ ":vcode" => $h,
+ ":username" => $username,
+ ":email" => $email
+ ));
+ if (!$success) {
+ $einfo = $stmt->errorInfo();
+ error_log("mcoop: send_validation_email failed updating members: " . var_export($einfo, true) . " ($h, $username, $email)");
+ throw new RegistrationError("database", "internal error generating validation code, contact the admin");
+ }
+ $headers = "From: " . $this->config->email_from_address;
+ $sitename = $this->config->website_name;
+ $vurl = urljoin($this->config->webapp_base_uri, "validate.php?un=$username&vcode=$h");
+ $deactivate_url = urljoin($this->config->webapp_base_uri, "deactivate.php?un=$username&vcode=$h");
+ $body = $email_twig_env->render("validation_email.tmpl", array(
+ "email" => $email,
+ "website_name" => $sitename,
+ "vurl" => $vurl,
+ "deactivate_url" => $deactivate_url
+ ));
+ mail($email, "Activate your $sitename account", $body, $headers);
+ }
+
+ function login($username, $password, $session_info) {
+ $m = DBMember::load_by($this, "username", $username);
+ // TODO: password reset logic
+ if (password_verify($password, $m->argon2_password_hash)) {
+ $_SESSION["logged_in"] = true;
+ $_SESSION["login_username"] = $username;
+ $session_info->re_init();
+ return true;
+ } else {
+ throw new LoginError("Invalid Password");
+ }
+ }
+
+ function get_members($only_verified=true) {
+
+ }
+}
+?>
diff --git a/common/db_classes.php b/common/db_classes.php
new file mode 100644
index 0000000..dde26eb
--- /dev/null
+++ b/common/db_classes.php
@@ -0,0 +1,346 @@
+<?php
+namespace mcoop;
+
+class TableCreationError extends \Exception {
+ public $table_name = null;
+ public $sql_error_code = null;
+ public $server_error_code = null;
+ public $server_error_message = null;
+
+ function __construct($table_name, $sql_error_code, $server_error_code, $server_error_message) {
+ $this->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"]);
+ }
+ }
+}
+?>
diff --git a/common/tables/dividend_credits.php b/common/tables/dividend_credits.php
new file mode 100644
index 0000000..2f80ec0
--- /dev/null
+++ b/common/tables/dividend_credits.php
@@ -0,0 +1,36 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+class DividendCreditsUpgrader extends BaseIncrementalTableUpgrader {
+ function from_1_to_2() {
+ //$this->conn->exec("ALTER TABLE dividend_ ADD COLUMN (test BOOL)");
+ // No actual changes in this one, it was just to make sure the upgrader paths were working
+ }
+
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "dividend_credits";
+ //$this->upgrade_method_names[2] = "from_1_to_2";
+ }
+}
+
+$dividend_credits_table_decl = new SimpleTableDecl(
+ "dividend_credits",
+ array(
+ "dividend_credits_ensure" => "INSERT IGNORE INTO dividend_credits (userid, year, credits, last_updated_table_version) VALUES (:userid, :year, 0, :table_ver)",
+ "add_to_dividend_credits" => "UPDATE dividend_credits SET credits=credits + :amount WHERE userid=:userid AND year=:year",
+ "remove_from_dividend_credits" => "UPDATE dividend_credits SET credits=credits - :amount WHERE userid=:userid AND year=:year",
+ "set_dividend_credits" => "UPDATE dividend_credits SET credits=:amount WHERE userid=:userid AND year=:year"
+ ),
+ "CREATE TABLE `dividend_credits` (
+`userid` INT NOT NULL,
+`year` INT NOT NULL,
+`credits` BIGINT NOT NULL,
+`last_updated_table_version` INT NOT NULL,
+PRIMARY KEY (`userid`, `year`)
+);",
+ 1,
+ "\mcoop\DividendCreditsUpgrader"
+);
+
diff --git a/common/tables/members.php b/common/tables/members.php
new file mode 100644
index 0000000..bf1240d
--- /dev/null
+++ b/common/tables/members.php
@@ -0,0 +1,47 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+class MembersUpgrader extends BaseIncrementalTableUpgrader {
+ function from_1_to_2() {
+ //$this->conn->exec("ALTER TABLE members ADD COLUMN (test BOOL)");
+ // No actual changes in this one, it was just to make sure the upgrader paths were working
+ }
+
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "members";
+ $this->upgrade_method_names[2] = "from_1_to_2";
+ }
+}
+
+$members_table_decl = new SimpleTableDecl(
+ "members",
+ array(
+ "register_member" => "INSERT INTO members (username, email, last_updated_table_version, full_name, argon2_password_hash) VALUES (:username, :email, :version, :full_name, :argon2_phash)",
+ "get_member_passhash_by_username" => "SELECT argon2_password_hash FROM members WHERE username = :username",
+ "get_member_by_username" => "SELECT * FROM members WHERE username = ?",
+ "get_member_by_email" => "SELECT * FROM members WHERE email = ?",
+ "get_members_by_uname_or_email" => "SELECT * FROM members WHERE username = :username OR email = :email",
+ "get_public_member_info_by_userid" => "SELECT userid, username, full_name FROM members WHERE userid=:userid"
+ ),
+ "CREATE TABLE `members` (
+`userid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+`username` TEXT CHARACTER SET utf8 NOT NULL,
+`email` TEXT CHARACTER SET utf8 NOT NULL,
+`last_updated_table_version` INT NOT NULL,
+`full_name` TEXT CHARACTER SET utf8,
+`validation_code` CHAR(64) CHARACTER SET ascii,
+`validated` BOOL DEFAULT false,
+`full_member` BOOL DEFAULT false,
+`argon2_password_hash` VARCHAR(256) CHARACTER SET ascii,
+`reset_password_hash` VARCHAR(256) CHARACTER SET ascii,
+`reset_requested` BOOL DEFAULT false,
+UNIQUE KEY (`username`(256)),
+UNIQUE KEY (`email`(256))
+);",
+ 2,
+ "\mcoop\MembersUpgrader"
+);
+
+?>
diff --git a/common/tables/messages.php b/common/tables/messages.php
new file mode 100644
index 0000000..65d821d
--- /dev/null
+++ b/common/tables/messages.php
@@ -0,0 +1,5 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+?>
diff --git a/common/tables/task_claims.php b/common/tables/task_claims.php
new file mode 100644
index 0000000..4f30964
--- /dev/null
+++ b/common/tables/task_claims.php
@@ -0,0 +1,32 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+class TaskClaimsUpgrader extends BaseIncrementalTableUpgrader {
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "task_claims";
+ }
+}
+
+$task_claims_table_decl = new SimpleTableDecl(
+ "task_claims",
+ array(
+ "create_task_claim" => "INSERT INTO task_claims (task_id, userid, last_updated_table_version, description) VALUES (:task_id, :userid, :table_ver, :description)",
+ // something that might be interesting is also keying on WHERE userid=:userid so the user that created it's the only one that can update it, I'll probably do something with perms though (that's a TODO)
+ "update_task_claim_desc" => "UPDATE task_claims SET description=:desc WHERE claim_id=:claim_id",
+ "get_task_claim_ids_by_taskid" => "SELECT claim_id FROM task_claims WHERE task_id=:task_id",
+ "get_task_claims_by_taskid" => "SELECT * FROM task_claims WHERE task_id=:task_id"
+ ),
+ "CREATE TABLE `task_claims` (
+`claim_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+`task_id` INT NOT NULL,
+`userid` INT NOT NULL,
+`last_updated_table_version` INT NOT NULL,
+`description` TEXT CHARACTER SET utf8
+);",
+ 1,
+ "\mcoop\TaskClaimsUpgrader"
+);
+
+?>
diff --git a/common/tables/task_dividend_credits.php b/common/tables/task_dividend_credits.php
new file mode 100644
index 0000000..d6a0ce7
--- /dev/null
+++ b/common/tables/task_dividend_credits.php
@@ -0,0 +1,32 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+class TaskDividendCreditsUpgrader extends BaseIncrementalTableUpgrader {
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "task_dividend_credits";
+ }
+}
+
+$task_dividend_credits_table_decl = new SimpleTableDecl(
+ "task_dividend_credits",
+ array(
+ "create_new_tdc" => "INSERT INTO task_dividend_credits (task_id, posted_userid, posted_by_coop, total_credits, remaining_credits, last_updated_table_version) VALUES (:task_id, :userid, :coop_post, :credits, :credits, :table_ver)",
+ "tdc_remove_credits" => "UPDATE task_dividend_credits SET remaining_credits=remaining_credits - :amount WHERE tdc_id = :tdc_id",
+ "get_tdcs_by_taskid" => "SELECT * FROM task_dividend_credits WHERE task_id=:task_id"
+ ),
+ "CREATE TABLE `task_dividend_credits` (
+`tdc_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+`task_id` INT NOT NULL,
+`posted_userid` INT NOT NULL,
+`posted_by_coop` BOOL NOT NULL,
+`total_credits` BIGINT NOT NULL,
+`remaining_credits` BIGINT NOT NULL,
+`last_updated_table_version` INT NOT NULL
+);",
+ 1,
+ "\mcoop\TaskDividendCreditsUpgrader"
+);
+
+?>
diff --git a/common/tables/tasks.php b/common/tables/tasks.php
new file mode 100644
index 0000000..af754d2
--- /dev/null
+++ b/common/tables/tasks.php
@@ -0,0 +1,45 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+class TasksUpgrader extends BaseIncrementalTableUpgrader {
+ function from_1_to_2() {
+ //$this->conn->exec("ALTER TABLE tasks ADD COLUMN (test BOOL)");
+ // No actual changes in this one, it was just to make sure the upgrader paths were working
+ }
+
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "tasks";
+ //$this->upgrade_method_names[2] = "from_1_to_2";
+ }
+}
+
+$tasks_table_decl = new SimpleTableDecl(
+ "tasks",
+ array(
+ "create_task" => "INSERT INTO tasks (admin_userid, last_updated_table_version, title, description) VALUES (:userid, :table_ver, :title, :desc)",
+ // TODO: dynamically generating these queries based on what fields are modified instead of statically would be a good idea
+ "update_task_title" => "UPDATE tasks SET title=:title WHERE taskid=:taskid",
+ "update_task_td" => "UPDATE tasks SET title=:title, description=:desc WHERE taskid=:taskid",
+ "update_task_desc" => "UPDATE tasks SET description=:description WHERE taskid=:taskid",
+ "get_all_tasks_simple" => "SELECT taskid, admin_userid, last_updated_table_version, title, state FROM tasks",
+ "get_all_tasks" => "SELECT * FROM tasks"
+ ),
+ "CREATE TABLE `tasks` (
+`taskid` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+`admin_userid` INT NOT NULL,
+`last_updated_table_version` INT NOT NULL,
+`title` TEXT CHARACTER SET utf8,
+`description` TEXT CHARACTER SET utf8,
+`state` ENUM ('open', 'closed') NOT NULL DEFAULT 'open'
+);",
+ 1,
+ "\mcoop\TasksUpgrader"
+);
+
+// TODO: tables for comments both on tasks and task claims would be a good idea (and probably necessary)
+
+// TODO: should either title be unique or maybe have an extra text task id kind of like bugzilla aliases?
+// TODO: need a claims table, and a table listing dividend credits available for each task, also a notification table for the task admin so they know when someone wants to add dividend credits to a closed task so they can reopen the task if deemed appropriate (also comments like any bugtracker?)
+?>
diff --git a/common/tables/tcc_history.php b/common/tables/tcc_history.php
new file mode 100644
index 0000000..362d702
--- /dev/null
+++ b/common/tables/tcc_history.php
@@ -0,0 +1,31 @@
+<?php
+namespace mcoop;
+require_once("common/db_classes.php");
+
+// tcc stands for Task Claim Credits
+
+class TccHistoryUpgrader extends BaseIncrementalTableUpgrader {
+ function __construct($conn) {
+ $this->conn = $conn;
+ $this->table_name = "tcc_history";
+ }
+}
+
+$tcc_history_table_decl = new SimpleTableDecl(
+ "tcc_history",
+ array(
+ "get_tcc_history_by_claimid" => "SELECT * FROM tcc_history WHERE to_claim_id=:claimid",
+ "get_tcc_history_by_tdc_id" => "SELECT * FROM tcc_history WHERE from_tdc_id=:tdc_id"
+ ),
+ "CREATE TABLE `tcc_history` (
+`from_tdc_id` INT NOT NULL,
+`to_claim_id` INT NOT NULL,
+`action` ENUM ('award', 'rescind') NOT NULL,
+`credits` BIGINT NOT NULL,
+`last_updated_table_version` INT NOT NULL
+);",
+ 1,
+ "\mcoop\TccHistoryUpgrader"
+);
+
+?>
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..6eaac62
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,7 @@
+{
+ "minimum-stability": "dev",
+ "require": {
+ "busybee/urljoin": "*",
+ "twig/twig": "^3.0@dev"
+ }
+}
diff --git a/css/mcoop.css b/css/mcoop.css
new file mode 100644
index 0000000..dbf51ee
--- /dev/null
+++ b/css/mcoop.css
@@ -0,0 +1,18 @@
+div.main_container {
+ background-color: #a59565;
+ position: relative;
+ padding: 0.75rem 1.25rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+ margin-right: -7px;
+ margin-left: -7px;
+}
+
+div.hidden_results_table {
+ visibility: hidden;
+}
+
+div.results_table {
+ visibility: visible;
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..7d5b10c
--- /dev/null
+++ b/index.php
@@ -0,0 +1,14 @@
+<?php
+namespace mcoop;
+
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+echo $twig->render("index.tmpl", array(
+ "danger_alerts" => array(),
+ "success_alerts" => array(),
+ "sess_info" => $sess_info,
+ "sitename" => $config->website_name
+));
+
+?>
diff --git a/js/tasks.js b/js/tasks.js
new file mode 100644
index 0000000..10483b3
--- /dev/null
+++ b/js/tasks.js
@@ -0,0 +1,46 @@
+function handle_search_results(res) {
+ //alert(res[0].title)
+ if (res) {
+ var tc = $("#table_container");
+ tc.removeClass("hidden_results_table");
+ // TODO: check if results_table is in tc's classes and add it if it isn't (jQuery stuff)
+ }
+ for (index in res) {
+ obj = res[index]
+ var tb = $("#results_tbody");
+ tb.empty();
+ //alert(tb);
+ var tr = document.createElement("tr");
+ var id_td = document.createElement("td");
+ var id_text = document.createTextNode(String(obj.taskid));
+ id_td.appendChild(id_text);
+ var title_td = document.createElement("td");
+ var title_text = document.createTextNode(obj.title);
+ title_td.appendChild(title_text);
+ var state_td = document.createElement("td");
+ var state_text = document.createTextNode(obj.state);
+ state_td.appendChild(state_text);
+ var admin_td = document.createElement("td");
+ var admin_text = document.createTextNode(obj.member_public.display_name);
+ admin_td.appendChild(admin_text);
+ var credits_total = 0;
+ for (tdc_index in obj.tdcs) {
+ tdc = obj.tdcs[tdc_index];
+ credits_total += Number(tdc.remaining_credits);
+ }
+ alert(credits_total);
+ var credits_td = document.createElement("td");
+ var credits_text = document.createTextNode(String(credits_total));
+ credits_td.appendChild(credits_text);
+ tr.appendChild(id_td);
+ tr.appendChild(title_td);
+ tr.appendChild(state_td);
+ tr.appendChild(admin_td);
+ tr.appendChild(credits_td);
+ tb.append(tr);
+ }
+}
+
+function get_all_tasks() {
+ fetch("tasks_api.php?action=get_all_tasks_simple").then(res => res.json()).then(res => handle_search_results(res));
+}
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..d310163
--- /dev/null
+++ b/login.php
@@ -0,0 +1,32 @@
+<?php
+namespace mcoop;
+
+//$logout = true;
+
+require_once("recaptcha/autoload.php");
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+$danger_alerts = array();
+$success_alerts = array();
+
+$login_attempted = false;
+if (isset($_POST["username"], $_POST["passwd"])) {
+ $login_attempted = true;
+ try {
+ $db->login($_POST["username"], $_POST["passwd"], $sess_info);
+ $success_alerts[] = 'Successfully logged in, <a href="/">Continue</a>';
+ } catch (UnknownMember $e) {
+ $danger_alerts[] = "Failed to login, no such user";
+ } catch (LoginError $e) {
+ $danger_alerts[] = "Failed to login, {$e->getMessage()}";
+ }
+}
+
+echo $twig->render("login.tmpl", array(
+ "danger_alerts" => $danger_alerts,
+ "success_alerts" => $success_alerts,
+ "sess_info" => $sess_info
+));
+
+?>
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..e8fbba1
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,15 @@
+<?php
+namespace mcoop;
+
+$logout = true;
+
+require_once("recaptcha/autoload.php");
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+echo $twig->render("logout.tmpl", array(
+ "danger_alerts" => array(),
+ "success_alerts" => array(),
+ "sess_info" => $sess_info
+));
+?>
diff --git a/profile.php b/profile.php
new file mode 100644
index 0000000..9240441
--- /dev/null
+++ b/profile.php
@@ -0,0 +1,115 @@
+<?php
+namespace mcoop;
+require_once("recaptcha/autoload.php");
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+// TODO urgent: I really need to ratelimit updating email addresses, otherwise there could be spam problems
+
+/* TODO: the template uses | escape('html_attr') even though the output's
+ supposed to be xhtml (though I'm still sending as text/html because
+ application/xhtml+xml seems to break recaptcha in current browsers),
+ fix that and make an escape filter for xml attributes
+*/
+
+if (!isset($sess_info->login_member)) {
+ header("Location: " . urljoin($config->webapp_base_uri, "login.php"));
+ exit();
+}
+
+$danger_alerts = array();
+$success_alerts = array();
+
+function check_full_name($db, $sess_info, $fullname) {
+ $filt_fullname = $db->validate_fullname($fullname);
+ return ((bool)$filt_fullname && ($filt_fullname != $sess_info->login_member->full_name));
+}
+
+function update_full_name($db, $sess_info, $fullname, $twig_env) {
+ global $success_alerts;
+ global $danger_alerts;
+ $filt_fullname = $db->validate_fullname($fullname);
+ $userid = $sess_info->login_member->userid;
+ $conn = $db->conn;
+ $st = $conn->prepare("UPDATE members SET full_name = ? WHERE userid = ?");
+ $success = $st->execute(array($filt_fullname, $userid));
+ if ($success) {
+ $success_alerts[] = "Full name updated successfully";
+ } else {
+ $einfo = $st->errorInfo();
+ error_log("mcoop: profile.php failed updating full_name: " . var_export($einfo, true) . " ($userid, $filt_fullname)");
+ $danger_alerts[] = "Internal error, please contact the admin";
+ }
+ return $success;
+}
+
+function check_email($db, $sess_info, $email) {
+ return ((bool)$email && ($email != $sess_info->login_member->email));
+}
+
+function update_email($db, $sess_info, $email, $twig_env) {
+ global $success_alerts;
+ global $danger_alerts;
+ $success = false;
+ $conn = $db->conn;
+ try {
+ $filt_email = $db->validate_email($email);
+ $userid = $sess_info->login_member->userid;
+ $username = $sess_info->login_member->username;
+ $conn->beginTransaction();
+ $st = $conn->prepare("UPDATE members SET email = ? , validated=false WHERE userid = ?");
+ $success = $st->execute(array($filt_email, $userid));
+ if ($success) {
+ $db->send_validation_email($username, $filt_email, $twig_env);
+ $conn->commit();
+ $success_alerts[] = "email updated successfully, you should get a new validation email at the new email address";
+ } else {
+ $einfo = $st->errorInfo();
+ error_log("mcoop: profile.php failed updating email: " . var_export($einfo, true) . " ($userid, $filt_email)");
+ $danger_alerts[] = "Internal error, please contact the admin";
+ }
+ } catch (RegistrationError $re) {
+ $success = false;
+ $danger_alerts[] = $re->reason;
+ if ($conn->inTransaction())
+ $conn->rollBack();
+ }
+ return $success;
+}
+
+
+// TODO: add password updating as well
+
+$update_vars = array();
+$varname_mappings = array(
+ "email" => array("\mcoop\check_email", "\mcoop\update_email"),
+ "fullname" => array("\mcoop\check_full_name", "\mcoop\update_full_name")
+);
+
+$attempted = false;
+
+foreach ($varname_mappings as $k => $a) {
+ if (isset($_POST[$k])) {
+ $v = $_POST[$k];
+ $check_func = $a[0];
+ $res = $check_func($db, $sess_info, $v);
+ if ($res) {
+ $attempted = true;
+ $update_func = $a[1];
+ $update_func($db, $sess_info, $v, $twig);
+ }
+ }
+}
+
+if ($attempted) {
+ $sess_info->re_init();
+}
+
+
+echo $twig->render("profile.tmpl", array(
+ "danger_alerts" => $danger_alerts,
+ "success_alerts" => $success_alerts,
+ "sess_info" => $sess_info
+));
+
+?>
diff --git a/register.php b/register.php
new file mode 100644
index 0000000..99cb2d1
--- /dev/null
+++ b/register.php
@@ -0,0 +1,57 @@
+<?php
+namespace mcoop;
+require_once("recaptcha/autoload.php");
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+// TODO: this should probably check if the user's already logged in and prompt to logout first
+
+//var_dump($_POST);
+
+$danger_alerts = array();
+$success_alerts = array();
+
+$reg_attempted = false;
+if (isset($_POST["username"], $_POST["email"], $_POST["passwd"], $_POST["g-recaptcha-response"])) {
+ $reg_attempted = true;
+ $recaptcha = new \ReCaptcha\ReCaptcha($config->recaptcha_secret);
+ $username = $_POST["username"];
+ $email = $_POST["email"];
+ $password = $_POST["passwd"];
+ if (isset($_POST["fullname"])) {
+ $full_name = $_POST["fullname"];
+ } else {
+ $full_name = null;
+ }
+ // TODO: we should really filter/validate g-recaptcha-response (still need to do)
+ $recaptcha_resp = $_POST["g-recaptcha-response"];
+ // XXX: one downside of this is it checks the captcha before validating all the other fields, might want to move captcha validation to register()
+ $resp = $recaptcha->verify($recaptcha_resp);
+ $captcha_valid = $resp->isSuccess();
+ $reg_successful = false;
+ if ($captcha_valid) {
+ try {
+ // TODO: validate_email (5th arg)
+ $db->register($username, $email, $password, $full_name, true, $twig);
+ $reg_successful = true;
+ $success_alerts[] = 'Registration successful, <a href="/">Click here to return to the webapp</a>';
+ } catch (RegistrationError $re) {
+ $reg_successful = false;
+ $error_text = $re->reason;
+ $danger_alerts[] = $error_text;
+ if ($db->conn->inTransaction())
+ $db->conn->rollBack();
+ }
+ } else {
+ $danger_alerts[] = "Captcha Invalid, please try again.";
+ }
+}
+
+// TODO: move into a util file
+echo $twig->render("register.tmpl", array(
+ "danger_alerts" => $danger_alerts,
+ "success_alerts" => $success_alerts,
+ "sess_info" => $sess_info
+));
+
+?>
diff --git a/tasks.php b/tasks.php
new file mode 100644
index 0000000..5834bdf
--- /dev/null
+++ b/tasks.php
@@ -0,0 +1,13 @@
+<?php
+require_once("common/config.php");
+
+$danger_alerts = array();
+$success_alerts = array();
+
+echo $twig->render("tasks.tmpl", array(
+ "danger_alerts" => $danger_alerts,
+ "success_alerts" => $success_alerts,
+ "sess_info" => $sess_info
+));
+
+?>
diff --git a/tasks_api.php b/tasks_api.php
new file mode 100644
index 0000000..e65b0e9
--- /dev/null
+++ b/tasks_api.php
@@ -0,0 +1,28 @@
+<?php
+namespace mcoop;
+require_once("common/config.php");
+
+function get_all_tasks_simple($db) {
+ $tasks = SimpleTask::get_all_simple($db);
+ foreach ($tasks as $k => $task) {
+ $task->load_admin($db);
+ $task->load_tdcs($db, false);
+ }
+ header("Content-Type: application/json");
+ // TODO: this should probably use tasks->to_json instead so as to not serialize more than needed
+ echo json_encode($tasks);
+ //var_dump($tasks);
+}
+
+$mapping = array(
+ "get_all_tasks_simple" => "\mcoop\get_all_tasks_simple"
+);
+
+if (isset($_GET["action"])) {
+ $action = $_GET["action"];
+ if (array_key_exists($action, $mapping)) {
+ $funcname = $mapping[$action];
+ $funcname($db);
+ }
+}
+?>
diff --git a/tmpl/base.tmpl b/tmpl/base.tmpl
new file mode 100644
index 0000000..b0fa3f3
--- /dev/null
+++ b/tmpl/base.tmpl
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>{% block title %}{% endblock %}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
+ <meta http-equiv="x-ua-compatible" content="ie=edge" />
+ {% block head_css_standard %}
+ <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css" />
+ <link rel="stylesheet" href="bower_components/tether/dist/css/tether.min.css" />
+ <link rel="stylesheet" href="css/mcoop.css" />
+ {% endblock %}
+ {% block head_extra %}{% endblock %}
+ </head>
+ <body>
+ <nav class="navbar navbar-dark bg-dark navbar-expand-lg">
+ <a class="navbar-brand" href="/">mcoop</a>
+ <div class="collapse navbar-collapse">
+ <ul class="navbar-nav mr-auto p-2">
+ <li class="nav-item"><a class="nav-link" href="tasks.php">Tasks</a></li>
+ <!--<li class="nav-item"><a class="nav-link" href="proposals.php">Proposals</a></li>
+ <li class="nav-item"><a class="nav-link" href="bylaws.php">Articles/Bylaws</a></li>-->
+ <li class="nav-item"><a class="nav-link" href="members.php">Members</a></li>
+ <!--<li class="nav-item"><a class="nav-link" href="directors.php">Directors</a></li>-->
+ </ul>
+ </div>
+ <ul class="navbar-nav ml-auto p-2">
+ {% if sess_info.login_member %}
+ <li class="nav-item dropdown">
+ <a class="nav-link dropdown-toggle" href="#" id="navbar_profile_dropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ sess_info.login_member.display_name | escape }}</a>
+ <div class="dropdown-menu" aria-labelledby="navbar_profile_dropdown">
+ <a class="dropdown-item" href="profile.php">Profile</a>
+ <a class="dropdown-item" href="logout.php">Logout</a>
+ </div>
+ </li>
+ {% else %}
+ <li class="nav-item ml-auto"><a class="nav-link" href="login.php">Login</a></li>
+ <li class="nav-item ml-auto"><a class="nav-link" href="register.php">Register</a></li>
+ {% endif %}
+ </ul>
+ </nav>
+ <div class="container-fluid">
+ {% block alerts %}
+ {% for alert in danger_alerts %}
+ <div class="alert alert-danger">
+ {{ alert | raw }}
+ </div>
+ {% endfor %}
+ {% for alert in success_alerts %}
+ <div class="alert alert-success">
+ {{ alert | raw }}
+ </div>
+ {% endfor %}
+ {% endblock %}
+
+ <div class="main_container">
+{% block page_contents %}{% endblock %}
+ </div>
+ </div>
+ {% block body_js %}<script src="bower_components/jquery/dist/jquery.min.js"></script>
+ <script src="bower_components/tether/dist/js/tether.min.js"></script>
+ <script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
+ {%- endblock %}
+ </body>
+</html>
diff --git a/tmpl/bylaws.tmpl b/tmpl/bylaws.tmpl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tmpl/bylaws.tmpl
diff --git a/tmpl/index.tmpl b/tmpl/index.tmpl
new file mode 100644
index 0000000..cea7e90
--- /dev/null
+++ b/tmpl/index.tmpl
@@ -0,0 +1,19 @@
+{% extends "base.tmpl" %}
+
+{% block title %}{{ sitename | escape }}{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h3>Welcome to {{ sitename | escape }}</h3></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ <div class="col-12"><p>This is a sample mcoop install, the intent of mcoop is to be used to manage small Co-Operatives, so far there aren't many features but here's a TODO list for some of the intended features</p></div>
+ <ul>
+ <li class="col-12">Allow both the Co-Operative and members to post bounties in terms of shares (or dividend credits) on specific tasks and let other members claim them once they've done an amount of work on the task.</li>
+ <li class="col-12">For Co-Operatives mostly focused on development allow automatic dividend credit distribution based on contributions to the Co-Operatives repositories, distributing x credits/month with the distribution based on kloc/person in commits or similar metrics (requires integration with VCSes -- also see Section 35 of C37-3, also worth noting that normal business with the Co-Operative should also yield dividend credits)</li>
+ <li class="col-12">Manage details of the directors and public-facing members for the public site ala. a simple CMS, or just integrate the list directly into mcoop and use it as your Co-Op's public site</li>
+ </ul>
+ <div class="alert alert-warning">
+ Disclaimer: Mainline is not an actual Co-Operative and isn't registered, the author is just interested in them and wanted to create a management webapp that might eventually be used by real Co-Operatives.
+ </div>
+ </div>
+{% endblock %}
diff --git a/tmpl/login.tmpl b/tmpl/login.tmpl
new file mode 100644
index 0000000..de3a6e9
--- /dev/null
+++ b/tmpl/login.tmpl
@@ -0,0 +1,18 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Login{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h2>Login</h2></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ </div>
+ <form action="login.php" method="post">
+ <div class="row">
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Username<span style="color: red">*</span></div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="text" name="username" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Password <span style="color: red">*</span></div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="password" name="passwd" /></div>
+ <div class="col-12"><button class="btn btn-success" type="submit">Login</button></div>
+ </div>
+{% endblock %}
diff --git a/tmpl/logout.tmpl b/tmpl/logout.tmpl
new file mode 100644
index 0000000..a507ed0
--- /dev/null
+++ b/tmpl/logout.tmpl
@@ -0,0 +1,9 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Logout{% endblock %}
+
+{% block page_contents %}
+<div class="d-flex flex-row">
+ <div class="d-flex justify-content-center col-12"><h5>Successfully Logged Out</h4></div>
+</div>
+{% endblock %}
diff --git a/tmpl/profile.tmpl b/tmpl/profile.tmpl
new file mode 100644
index 0000000..92bb780
--- /dev/null
+++ b/tmpl/profile.tmpl
@@ -0,0 +1,24 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Profile{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h2>Edit Profile</h2></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ </div>
+ <form action="profile.php" method="post">
+ <div class="row">
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Username</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10">{{ sess_info.login_member.username }}</div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Email</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="email" name="email" value="{{ sess_info.login_member.email | escape('html_attr') }}" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Full name</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="text" name="fullname" value="{{ sess_info.login_member.full_name | escape('html_attr') }}" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Old Password</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="password" name="old_passwd" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">New Password</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="password" name="new_passwd" /></div>
+ <div class="col-12"><button class="btn btn-success" type="submit">Update</button></div>
+ </div>
+{% endblock %}
diff --git a/tmpl/register.tmpl b/tmpl/register.tmpl
new file mode 100644
index 0000000..f0878e9
--- /dev/null
+++ b/tmpl/register.tmpl
@@ -0,0 +1,30 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Register{% endblock %}
+
+{% block body_js %}
+<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl=en"></script>
+ {{- parent() }}
+{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h2>Register for basic membership here</h2></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ <div class="col-12"><p>this doesn't register you as a full member, you have to confirm your email address and start <a href="contribute.xhtml">Contributing</a> first</p></div>
+ </div>
+ <form action="register.php" method="post">
+ <div class="row">
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Username<span style="color: red">*</span></div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="text" name="username" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Email Address <span style="color: red">*</span></div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="email" name="email" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Password <span style="color: red">*</span></div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="password" name="passwd" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Full Name</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input name="fullname" /></div>
+ <div class="offset-sm-2 col-11"><div class="g-recaptcha" data-sitekey="6Le5mkIUAAAAAKBvBdwIqcWd9vVMV2t9YhGxfMGb"></div></div>
+ <div class="col-12"><button class="btn btn-success" type="submit">Register</button></div>
+ </div>
+ </form>
+{% endblock %}
diff --git a/tmpl/tasks.tmpl b/tmpl/tasks.tmpl
new file mode 100644
index 0000000..070e002
--- /dev/null
+++ b/tmpl/tasks.tmpl
@@ -0,0 +1,29 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Tasks{% endblock %}
+
+{% block head_extra %}
+<script src="js/tasks.js"></script>
+{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h2>Task Search</h2></div>
+ <div class="col-12"><button class="btn btn-secondary" onclick="get_all_tasks()" name="view_all">View All Tasks</button></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ </div>
+ <div id="table_container" class="hidden_results_table">
+ <table id="results_table" class="table">
+ <thead><tr>
+ <td>ID</td>
+ <td>Title</td>
+ <td>State</td>
+ <td>Admin</td>
+ <td>Available Credits</td>
+ </tr></thead>
+ <tbody id="results_tbody">
+
+ </tbody>
+ </thead>
+ </div>
+{% endblock %}
diff --git a/tmpl/validate.tmpl b/tmpl/validate.tmpl
new file mode 100644
index 0000000..ae32718
--- /dev/null
+++ b/tmpl/validate.tmpl
@@ -0,0 +1,19 @@
+{% extends "base.tmpl" %}
+
+{% block title %}Validate Email{% endblock %}
+
+{% block page_contents %}
+ <div class="row">
+ <div class="col-12"><h2>Validate Email Address</h2></div>
+ <div class="col-12"><hr style="border: 1px solid rgba(0, 0, 0, 1)"/></div>
+ <div class="col-12"><p>If for whatever reason you can&apos;t click on the link in the email you can manually enter your username and the validation code here to validate your email address, the validation code is everything to the right of vcode= (excluding the &apos;=&apos;) in the url</p></div>
+ </div>
+ <form action="validate.php" method="get">
+ <div class="row">
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Username</div>
+ <div class="col-12 col-sm-8 col-md-9 col-lg-10"><input type="text" name="un" /></div>
+ <div class="col-12 col-sm-4 col-md-3 col-lg-2">Validation Code</div>
+ <div class="col-12 col-sm-8 col-md-3 col-lg-10"><input type="text" name="vcode" /></div>
+ <div class="col-12"><button class="btn btn-success" type="submit">Validate Email</button></div>
+ </div>
+{% endblock %}
diff --git a/tmpl/validation_email.tmpl b/tmpl/validation_email.tmpl
new file mode 100644
index 0000000..35076a4
--- /dev/null
+++ b/tmpl/validation_email.tmpl
@@ -0,0 +1,9 @@
+Hello {{ email }},
+
+Thank you for registering at {{ website_name }}, to verify your email address please visit the following URL:
+{{ vurl | raw }}
+
+If you didn't create an account you can either do nothing, in which case the person that tried registering with your email address will continue having limited access to the website until they change their email address, or request a deactivation at:
+{{ deactivate_url | raw }}
+
+Deactivated accounts are deleted automatically after 28 days unless a reactivation is requested.
diff --git a/todo.txt b/todo.txt
new file mode 100644
index 0000000..f02c3b2
--- /dev/null
+++ b/todo.txt
@@ -0,0 +1,25 @@
+* Tasks and Member list of course
+* Bylaw management
+ * shares distribution policy, a few things about that are in 7(2) and 8(1)
+ * shareholder classes
+ * share classes
+ * Voting on Bylaw proposals by members
+* Something that might make sense is preparing for initial incorporation,
+ utilities for getting everything in order (only thing is I'm not an expert
+ at co-operatives)
+ * initially it would only handle Saskatchewan requirements, eventually
+ create a DSL or a method of configuration to handle the different
+ laws and requirements in different jurisdictions
+* Receiver stuff, see Part VIII (might just add a table of Receiverships for
+ specific properties, or as a column in the members table)
+* General meetings things, Part IX
+* Resolutions and
+* committees, see section 73
+* director elections, see 74
+* Voting on sales, leases and exchanges as per 76
+* Specific officer roles, Section 94
+* Getting into the Membership things is more relevant, Part X (Section 98+)
+* 106 record dates are relevant
+* 110(3) makes sense for including in Bylaws/Articles or the site settings
+* 111 mentions Proposals
+* 112 has bylaw stuff
diff --git a/validate.php b/validate.php
new file mode 100644
index 0000000..26fc67d
--- /dev/null
+++ b/validate.php
@@ -0,0 +1,40 @@
+<?php
+namespace mcoop;
+require_once("recaptcha/autoload.php");
+require_once("vendor/autoload.php");
+require_once("common/config.php");
+
+$danger_alerts = array();
+$success_alerts = array();
+
+$vattempted = false;
+if (isset($_GET["un"], $_GET["vcode"])) {
+ $vattempted = true;
+ try {
+ $m = DBMember::load_by($db, "username", $_GET["un"]);
+ if ($m->validation_code == $_GET["vcode"]) {
+ $st = $db->conn->prepare("UPDATE members SET validated=true WHERE userid = ?");
+ $success = $st->execute(array($m->userid));
+ if ($success) {
+ $success_alerts[] = 'Successfully validated your email address, <a href="/">Continue</a>';
+ } else {
+ $einfo = $st->errorInfo();
+ error_log("mcoop: validate.php failed updating members: " . var_export($einfo, true) . " ($m->userid)");
+ $danger_alerts[] = "Failed to update the database to validate your email address, this is an internal error, please contact the admin";
+ }
+ } else {
+ $danger_alerts[] = "Unable to validate email: incorrect validation code";
+ }
+ } catch (UnknownMember $un) {
+ $danger_alerts[] = "Unable to validate email: no such user";
+ }
+}
+
+echo $twig->render("validate.tmpl", array(
+ "danger_alerts" => $danger_alerts,
+ "success_alerts" => $success_alerts,
+ "sess_info" => $sess_info,
+ "vattempted" => $vattempted
+));
+
+?>