From 3355affd6c11b6ab32015bd1eed4306bd020b56b Mon Sep 17 00:00:00 2001 From: M Date: Sat, 21 Nov 2015 17:37:18 -0500 Subject: + Committing initial framework class definition --- app/class/framework.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/class/framework.class.php (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php new file mode 100644 index 0000000..452f0c3 --- /dev/null +++ b/app/class/framework.class.php @@ -0,0 +1,26 @@ + -- cgit v1.2.3 From adade14d9e386797a65f1beb405c21ebbff1ca37 Mon Sep 17 00:00:00 2001 From: M Date: Sat, 21 Nov 2015 18:25:28 -0500 Subject: + Adding abstract controller class --- app/class/controller.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/class/controller.class.php (limited to 'app/class') diff --git a/app/class/controller.class.php b/app/class/controller.class.php new file mode 100644 index 0000000..4ea40d1 --- /dev/null +++ b/app/class/controller.class.php @@ -0,0 +1,17 @@ + -- cgit v1.2.3 From c50a6be054db3ddb260585865df8341e1347ad73 Mon Sep 17 00:00:00 2001 From: M Date: Sat, 21 Nov 2015 20:53:44 -0500 Subject: * Framework def file is now condifionally including system-level app configuration --- app/class/framework.class.php | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 452f0c3..e20be7f 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -1,5 +1,9 @@ Date: Sat, 21 Nov 2015 21:47:03 -0500 Subject: + Defined function to check if scrott.conf.php file exists --- app/class/framework.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index e20be7f..5232135 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -9,6 +9,15 @@ is_file("scrott.conf.php") && */ abstract class Framework { + /* + * Check for the existence of Scrott's system-level config + */ + function scrottConfExists() + { + global $_SCROTT; + return isset($_SCROTT['conf']); + } + /* * Get the absolute path on this server for the root of this app */ -- cgit v1.2.3 From dfa67b6059c9657454d3abed2e66ce30ce168960 Mon Sep 17 00:00:00 2001 From: M Date: Sat, 21 Nov 2015 23:17:11 -0500 Subject: + Added abstract model definition --- app/class/model.class.php | 71 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/class/model.class.php (limited to 'app/class') diff --git a/app/class/model.class.php b/app/class/model.class.php new file mode 100644 index 0000000..25e34ab --- /dev/null +++ b/app/class/model.class.php @@ -0,0 +1,71 @@ +errorlist = array(); + $this->warninglist = array(); + $this->noticelist = array(); + } + + /* + * Check for error + */ + function isError() + { + return count($this->errorlist) > 0; + } + + /* + * Check for warning + */ + function isWarning() + { + return count($this->warninglist) > 0; + } + + /* + * Check for notice + */ + function isNotice() + { + return count($this->noticelist) > 0; + } + + /* + * Log an error + */ + function logError($str) + { + $this->errorlist[] = $str; + } + + /* + * Log a warning + */ + function logWarning($str) + { + $this->warninglist[] = $str; + } + + /* + * Log a notice + */ + function logNotice($str) + { + $this->noticelist[] = $str; + } +} + +?> -- cgit v1.2.3 From debd679aa4e3d7eb2d216e57df859dd9e6427f5f Mon Sep 17 00:00:00 2001 From: M Date: Sun, 22 Nov 2015 00:24:37 -0500 Subject: * Implemented framework ar (app root) function --- app/class/framework.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 5232135..11902d0 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -23,7 +23,7 @@ abstract class Framework */ function ar() { - /* TODO */ + return substr($_SERVER['PHP_SELF'], 0, -10); // 10 = length of "/index.php" } /* -- cgit v1.2.3 From 2710f0de8d8d900a0997fd72f315c8a6f07329cf Mon Sep 17 00:00:00 2001 From: M Date: Sun, 22 Nov 2015 03:04:06 -0500 Subject: * Derp, default is a reserved word, calling the function 'deflt' instead * Removed explicit call to parent constructor in model class, since that function is not explicitly defined --- app/class/model.class.php | 2 -- 1 file changed, 2 deletions(-) (limited to 'app/class') diff --git a/app/class/model.class.php b/app/class/model.class.php index 25e34ab..4f597f7 100644 --- a/app/class/model.class.php +++ b/app/class/model.class.php @@ -12,8 +12,6 @@ abstract class Model extends Framework */ function __construct() { - parent::__construct(); - $this->errorlist = array(); $this->warninglist = array(); $this->noticelist = array(); -- cgit v1.2.3 From 49e6128951e8d8b340ea6027735c8b3566c44b6b Mon Sep 17 00:00:00 2001 From: M Date: Thu, 3 Dec 2015 22:11:13 -0500 Subject: + Started Form class definition --- app/class/form.class.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/class/form.class.php (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php new file mode 100644 index 0000000..e398690 --- /dev/null +++ b/app/class/form.class.php @@ -0,0 +1,35 @@ +textFields = array(); + + $this->errorlist = array(); + $this->warninglist = array(); + $this->noticelist = array(); + } + + /* + * Add new text field to the form + */ + function field_text($name, $req = true) + { + if ($req !== true) + $req = false; + + $this->textFields[] = array( + 'name' => $name, + 'req' => $req + ); + } +} + +?> -- cgit v1.2.3 From 59962f7c260aaa0661b0c811e6b553d1a850032b Mon Sep 17 00:00:00 2001 From: M Date: Sat, 5 Dec 2015 15:03:48 -0500 Subject: + Added numeric and enum types to Form class --- app/class/form.class.php | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index e398690..ffee3d7 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -11,6 +11,8 @@ class Form function __construct() { $this->textFields = array(); + $this->numbFields = array(); + $this->enumFields = array(); $this->errorlist = array(); $this->warninglist = array(); @@ -30,6 +32,41 @@ class Form 'req' => $req ); } + + /* + * Add new numeric field to the form + */ + function field_numeric($name, $req = true, $integer = true, $min = null, $max = null) + { + if ($req !== true) + $req = false; + + if ($integer !== true) + $integer = false; + + $this->numbFields[] = array( + 'name' => $name, + 'req' => $req, + 'int' => $integer, + 'min' => $min, + 'max' => $max + ); + } + + /* + * Add new enumeration field to the form + */ + function field_enum($name, $req = true, $values) + { + if ($req !== true) + $req = false; + + $this->enumFields[] = array( + 'name' => $name, + 'req' => $req, + 'vals' => $values + ); + } } ?> -- cgit v1.2.3 From 91659b121e63735a7620663c0f43f5c5adef77d4 Mon Sep 17 00:00:00 2001 From: M Date: Sat, 5 Dec 2015 18:54:01 -0500 Subject: + Implemented populate function in Form class + Added helper function in Form class, logError ! Finished Form class for now --- app/class/form.class.php | 100 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 97 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index ffee3d7..502e348 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -14,9 +14,15 @@ class Form $this->numbFields = array(); $this->enumFields = array(); - $this->errorlist = array(); - $this->warninglist = array(); - $this->noticelist = array(); + $this->errorlist = array(); + } + + /* + * Log an error + */ + function logError($str) + { + $this->errorlist[] = $str; } /* @@ -67,6 +73,94 @@ class Form 'vals' => $values ); } + + /* + * Populate the form with input data from web page + */ + function populate($input) + { + /* detect duplicate names */ + $names = array(); + foreach ($this->textFields as $fld) + $names[] = $fld['name']; + foreach ($this->numbFields as $fld) + $names[] = $fld['name']; + foreach ($this->enumFields as $fld) + $names[] = $fld['name']; + + if (count(array_unique($names)) != count($names)) + { + $this->logError("Internal error: Duplicate field names defined in form"); + return false; + } + + /* init text fields */ + foreach ($this->textFields as $fld) + { + if (isset($input[$fld['name']])) + $this->$fld['name'] = htmlEntities($input[$fld['name']], ENT_QUOTES); + + else if ($fld['req']) + $this->logError($fld['name'] . " is required"); + } + + /* init numeric fields */ + foreach ($this->numbFields as $fld) + { + if (isset($input[$fld['name']])) + { + if (!is_numeric($input[$fld['name']])) + { + $this->logError($fld['name'] . " must be numeric"); + continue; + } + + if ($fld['int'] && (floor($input[$fld['name']]) != $input[$fld['name']])) + { + $this->logError($fld['name'] . " must be an integer"); + continue; + } + + if (!is_null($fld['min']) && ($input[$fld['name']] < $fld['min'])) + { + $this->logError($fld['name'] . " must be no less than " . $fld['min']); + continue; + } + + if (!is_null($fld['max']) && ($input[$fld['name']] > $fld['max'])) + { + $this->logError($fld['name'] . " must be no more than " . $fld['max']); + continue; + } + + $this->$fld['name'] = $input[$fld['name']]; + } + + else if ($fld['req']) + $this->logError($fld['name'] . " is required"); + } + + /* init enum fields */ + foreach ($this->enumFields as $fld) + { + if (isset($input[$fld['name']])) + { + if (array_search($input[$fld['name']], $fld['vals']) === false) + { + $this->logError($fld['name'] . " is not an appropriate value"); + continue; + } + + $this->$fld['name'] = $input[$fld['name']]; + } + + else if ($fld['req']) + $this->logError($fld['name'] . " is required"); + } + + /* return */ + return count($this->errorlist) == 0; + } } ?> -- cgit v1.2.3 From 9bab1e5c3d7dae9603c5f2172b2a620465caab0e Mon Sep 17 00:00:00 2001 From: M Date: Sat, 5 Dec 2015 21:37:03 -0500 Subject: * Form class fields now have the ability to set a default value. Default value is applied if the supplied $input array has no key matching the field name. --- app/class/form.class.php | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 502e348..e50876d 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -28,21 +28,22 @@ class Form /* * Add new text field to the form */ - function field_text($name, $req = true) + function field_text($name, $deflt = null, $req = true) { if ($req !== true) $req = false; $this->textFields[] = array( - 'name' => $name, - 'req' => $req + 'name' => $name, + 'deflt' => $deflt, + 'req' => $req ); } /* * Add new numeric field to the form */ - function field_numeric($name, $req = true, $integer = true, $min = null, $max = null) + function field_numeric($name, $min = null, $max = null, $deflt = null, $integer = true, $req = true) { if ($req !== true) $req = false; @@ -51,26 +52,28 @@ class Form $integer = false; $this->numbFields[] = array( - 'name' => $name, - 'req' => $req, - 'int' => $integer, - 'min' => $min, - 'max' => $max + 'name' => $name, + 'min' => $min, + 'max' => $max, + 'deflt' => $deflt, + 'int' => $integer, + 'req' => $req ); } /* * Add new enumeration field to the form */ - function field_enum($name, $req = true, $values) + function field_enum($name, $values, $deflt = null, $req = true) { if ($req !== true) $req = false; $this->enumFields[] = array( - 'name' => $name, - 'req' => $req, - 'vals' => $values + 'name' => $name, + 'vals' => $values, + 'deflt' => $deflt, + 'req' => $req ); } @@ -100,6 +103,9 @@ class Form if (isset($input[$fld['name']])) $this->$fld['name'] = htmlEntities($input[$fld['name']], ENT_QUOTES); + else if (!is_null($fld['deflt'])) + $this->$fld['name'] = $fld['deflt']; + else if ($fld['req']) $this->logError($fld['name'] . " is required"); } @@ -136,6 +142,9 @@ class Form $this->$fld['name'] = $input[$fld['name']]; } + else if (!is_null($fld['deflt'])) + $this->$fld['name'] = $fld['deflt']; + else if ($fld['req']) $this->logError($fld['name'] . " is required"); } @@ -154,6 +163,9 @@ class Form $this->$fld['name'] = $input[$fld['name']]; } + else if (!is_null($fld['deflt'])) + $this->$fld['name'] = $fld['deflt']; + else if ($fld['req']) $this->logError($fld['name'] . " is required"); } -- cgit v1.2.3 From 5a05468fe2d78641d3adb0ba5b83bf526f4f06de Mon Sep 17 00:00:00 2001 From: M Date: Sat, 5 Dec 2015 22:53:34 -0500 Subject: + Added framework function for getting current app path * Changed sysconf view to use new function ($mod->ar()/sysconf -> $mod->ap) --- app/class/framework.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 11902d0..151ca8e 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -26,6 +26,14 @@ abstract class Framework return substr($_SERVER['PHP_SELF'], 0, -10); // 10 = length of "/index.php" } + /* + * Get the absolute path to the current page + */ + function ap() + { + return $this->ar() . $_REQUEST['path']; + } + /* * Redirect to the given URL and die */ -- cgit v1.2.3 From e6f3bf746fbb1d4c768a1d43e2a0233d0fb25f47 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 6 Dec 2015 00:12:16 -0500 Subject: * Bug fix in Form class - populate function -- If a field was set in $input, but equal to "", the isset check would not behave as expected --- app/class/form.class.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index e50876d..d3af399 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -100,7 +100,7 @@ class Form /* init text fields */ foreach ($this->textFields as $fld) { - if (isset($input[$fld['name']])) + if (isset($input[$fld['name']]) && $input[$fld['name']] != "") $this->$fld['name'] = htmlEntities($input[$fld['name']], ENT_QUOTES); else if (!is_null($fld['deflt'])) @@ -113,7 +113,7 @@ class Form /* init numeric fields */ foreach ($this->numbFields as $fld) { - if (isset($input[$fld['name']])) + if (isset($input[$fld['name']]) && $input[$fld['name']] != "") { if (!is_numeric($input[$fld['name']])) { @@ -152,7 +152,7 @@ class Form /* init enum fields */ foreach ($this->enumFields as $fld) { - if (isset($input[$fld['name']])) + if (isset($input[$fld['name']]) && $input[$fld['name']] != "") { if (array_search($input[$fld['name']], $fld['vals']) === false) { -- cgit v1.2.3 From 9f9d2a9d313122e9cf365e3baf4a8889b611ae28 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 6 Dec 2015 00:16:14 -0500 Subject: + Added function to model class to log all error messages from a Form objects populate call --- app/class/model.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/model.class.php b/app/class/model.class.php index 4f597f7..85bcf54 100644 --- a/app/class/model.class.php +++ b/app/class/model.class.php @@ -64,6 +64,14 @@ abstract class Model extends Framework { $this->noticelist[] = $str; } + + /* + * Log errors from a Form + */ + function logFormErrors($obj) + { + $this->errorlist = array_merge($this->errorlist, $obj->errorlist); + } } ?> -- cgit v1.2.3 From 366e538edd1a63143ddc229679d3d8be285a9ec3 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 6 Dec 2015 03:10:13 -0500 Subject: * Bug fix in framework class - redirectTo function -- http_redirect function I was using is part of an extension for PHP and therefore, non-standard --- app/class/framework.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 151ca8e..7244220 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -39,7 +39,7 @@ abstract class Framework */ function redirectTo($url) { - http_redirect($url); + header("Location: " . $url); exit; } } -- cgit v1.2.3 From 2896ade5e1257045513f871d59e6e4eaac27e317 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 8 Dec 2015 18:51:20 -0500 Subject: + Added bool field type to Form class --- app/class/form.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index d3af399..808de27 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -77,6 +77,14 @@ class Form ); } + /* + * Add new boolean field to the form + */ + function field_bool($name) + { + $this->field_enum($name, array("true", "false"), "false"); + } + /* * Populate the form with input data from web page */ -- cgit v1.2.3 From 2dd0900cd5c2adb610fd35e10133dd9fc10ca0f9 Mon Sep 17 00:00:00 2001 From: M Date: Tue, 8 Dec 2015 19:21:46 -0500 Subject: + Added controller security assertions: require_https and forbid_https --- app/class/controller.class.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'app/class') diff --git a/app/class/controller.class.php b/app/class/controller.class.php index 4ea40d1..fabd7e7 100644 --- a/app/class/controller.class.php +++ b/app/class/controller.class.php @@ -12,6 +12,26 @@ abstract class Controller extends Framework * Abstract function for concrete controller to handle the page request */ abstract function handle($argv); + + /* + * Security check + * Assert that the current connection to this server is secure. Redirects if not. + */ + function sec_require_https() + { + if (!isset($_SERVER['HTTPS'])) + $this->redirectTo("https://" . $_SERVER['SERVER_NAME'] . $this->ap()); + } + + /* + * Security check + * Assert that the current connection to this server is NOT secure. Redirects if not. + */ + function sec_forbid_https() + { + if (isset($_SERVER['HTTPS'])) + $this->redirectTo("http://" . $_SERVER['SERVER_NAME'] . $this->ap()); + } } ?> -- cgit v1.2.3 From bdc8790368e2f8b247c8492507d4083ddfbd61c1 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 17 Dec 2015 00:39:54 -0500 Subject: + Added generic database interface to use throughout the app since I'm planning on supporting multiple database engines + Defined interface for Mysql DBMS for Scrott --- app/class/database.iface.php | 13 +++++++++ app/class/mysql.class.php | 63 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 app/class/database.iface.php create mode 100644 app/class/mysql.class.php (limited to 'app/class') diff --git a/app/class/database.iface.php b/app/class/database.iface.php new file mode 100644 index 0000000..dcd64ba --- /dev/null +++ b/app/class/database.iface.php @@ -0,0 +1,13 @@ + diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php new file mode 100644 index 0000000..b08257f --- /dev/null +++ b/app/class/mysql.class.php @@ -0,0 +1,63 @@ +db = new mysqli($host, $username, $password, $dbName); + + if ($this->db->connect_error) + throw new Exception("Can not connect to Mysql database. Please check your Scrott configuration."); + } + + /* + * Destructor + */ + function __destruct() + { + $this->close(); + } + + /* + * Close connection to DB + */ + function close() + { + $this->db->close(); + } + + /* + * Make a query of the database. Return data as an array of arrays + */ + function query($query) + { + $arr = array(); + $res = $this->db->query($query); + + if ($res === true || $res === false) + return $arr; + + foreach ($res as $r) + $arr[] = $r->fetch_assoc(); + + return $arr; + } + + /* + * Escape a string for use in a query + */ + function esc($string) + { + return $this->db->real_escape_string($string); + } +} + +?> -- cgit v1.2.3 From 886bc202b8debe29f0c3e70b027ad3202e78c263 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 17 Dec 2015 01:36:09 -0500 Subject: + Added function to framework class for getting (or creating) the app's singleton db connection object. If no connection is established, logic uses system-level configuration to decide how to connect before returning --- app/class/framework.class.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 7244220..eea6c25 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -4,11 +4,15 @@ is_file("scrott.conf.php") && require_once "scrott.conf.php"; +require_once "class/mysql.class.php"; + /* * Global functions / operations and access to contextual or session-based information */ abstract class Framework { + static $dbobj = null; + /* * Check for the existence of Scrott's system-level config */ @@ -42,6 +46,34 @@ abstract class Framework header("Location: " . $url); exit; } + + /* + * Get or create the app's database connection object (this is a singleton object and dependent on system-level config) + */ + function getDbConnection() + { + global $_SCROTT; + + if (self::$dbobj != null) + return self::$dbobj; + + switch ($_SCROTT['dbEngine']) + { + case "mysql": + $host = $_SCROTT['dbAddress']; + $username = $_SCROTT['dbUser']; + $password = $_SCROTT['dbPass']; + $dbName = $_SCROTT['dbName']; + self::$dbobj = new Mysql($host, $username, $password, $dbName); + break; + + default: + throw new Exception("Problem with Scrott Configuration. Invalid database engine specified."); + break; + } + + return self::$dbobj; + } } ?> -- cgit v1.2.3 From 0f9b65d812b601c5e047838b07b96098cbe8ad35 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 17 Dec 2015 13:21:49 -0500 Subject: * Bug fix in Mysql support class -- misuse of Mysql result object and its member function fetch_assoc --- app/class/mysql.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php index b08257f..317468c 100644 --- a/app/class/mysql.class.php +++ b/app/class/mysql.class.php @@ -45,8 +45,8 @@ class Mysql implements Database if ($res === true || $res === false) return $arr; - foreach ($res as $r) - $arr[] = $r->fetch_assoc(); + while ($r = $res->fetch_assoc()) + $arr[] = $r; return $arr; } -- cgit v1.2.3 From c31231740866fc31f9f40f9cf53555efec032291 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 17 Dec 2015 13:25:08 -0500 Subject: + Added abstract base class for Scrott database objects (implemented constructor and loadObj functions) --- app/class/object.class.php | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/class/object.class.php (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php new file mode 100644 index 0000000..4d00009 --- /dev/null +++ b/app/class/object.class.php @@ -0,0 +1,71 @@ +db = $this->getDbConnection(); + + $this->table = "object"; + $this->cols = array( + "guid", + "perms", + "owner", + "parent", + "name", + "timeCreated", + "timeUpdated", + "type" + ); + + $this->childTable = $this->db->esc($childTable); + $this->childCols = array(); + + if (is_array($childCols)) + { + foreach ($childCols as $col) + $this->childCols[] = $this->db->esc($col); + } + } + + /* + * Populate this object with data from the DB with a given GUID + */ + function loadObj($guid) + { + if (is_null($guid)) + return; + + $escdGuid = $this->db->esc($guid); + + /* Common fields */ + $query = "SELECT * FROM `" . $this->table . "` WHERE `guid` = '" . $escdGuid . "'"; + $result = $this->db->query($query)[0]; + + foreach ($this->cols as $col) + { + if (isset($result[$col])) + $this->$col = $result[$col]; + } + + /* Child Table fields */ + $query = "SELECT * FROM `" . $this->childTable . "` WHERE `guid` = '" . $escdGuid . "'"; + $result = $this->db->query($query)[0]; + + foreach ($this->childCols as $col) + { + if (isset($result[$col])) + $this->$col = $result[$col]; + } + } +} + +?> -- cgit v1.2.3 From 2d674ddde9b02a5800e7b7004bc7453305e5862c Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 00:34:15 -0500 Subject: + Added saveObj function to Object class --- app/class/object.class.php | 94 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 4d00009..fb38ef7 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -46,7 +46,7 @@ abstract class Object extends Framework $escdGuid = $this->db->esc($guid); - /* Common fields */ + /* Base fields */ $query = "SELECT * FROM `" . $this->table . "` WHERE `guid` = '" . $escdGuid . "'"; $result = $this->db->query($query)[0]; @@ -66,6 +66,98 @@ abstract class Object extends Framework $this->$col = $result[$col]; } } + + /* + * Write this object to the database + */ + function saveObj() + { + if (isset($this->guid)) + { + /* Update Base */ + $updateStr = ""; + + foreach ($this->cols as $col) + { + if (!isset($this->$col)) + continue; + + $updateStr .= "`" . $col . "` = '" . $this->db->esc($this->$col) . "', "; + } + + if (strlen($updateStr) > 0) + { + $updateStr = substr($updateStr, 0, -2); // remove ", " from the end + $query = "UPDATE `" . $this->table . "` SET " . $updateStr . " WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + } + + /* Update Child */ + $updateStr = ""; + + foreach ($this->childCols as $col) + { + if (!isset($this->$col)) + continue; + + $updateStr .= "`" . $col . "` = '" . $this->db->esc($this->$col) . "', "; + } + + if (strlen($updateStr) > 0) + { + $updateStr = substr($updateStr, 0, -2); // remove ", " from the end + $query = "UPDATE `" . $this->childTable . "` SET " . $updateStr . " WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + } + } + + else + { + $this->guid = $this->getNewGUID(); + + /* Insert Base */ + $colsStr = ""; + $valsStr = ""; + + foreach ($this->cols as $col) + { + if (!isset($this->$col)) + continue; + + $colsStr .= "`" . $col . "`, "; + $valsStr .= "'" . $this->db->esc($this->$col) . "', "; + } + + if (strlen($colsStr) > 0) + { + $colsStr = substr($colsStr, 0, -2); // remove ", " + $valsStr = substr($valsStr, 0, -2); + $query = "INSERT INTO `" . $this->table . "` (" . $colsStr . ") VALUES (" . $valsStr . ")"; + $this->db->query($query); + } + + /* Insert Child */ + $colsStr = ""; + $valsStr = ""; + + foreach ($this->childCols as $col) + { + if (!isset($this->$col)) + continue; + + $colsStr .= "`" . $col . "`, "; + $valsStr .= "'" . $this->db->esc($this->$col) . "', "; + } + + if (strlen($colsStr) > 0) + { + $colsStr = substr($colsStr, 0, -2); // remove ", " + $valsStr = substr($valsStr, 0, -2); + $query = "INSERT INTO `" . $this->childTable . "` (" . $colsStr . ") VALUES (" . $valsStr . ")"; + $this->db->query($query); + } + } + } } ?> -- cgit v1.2.3 From 6bc0491af4349a03a2d9f2040f36901aa5497d0d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 01:03:40 -0500 Subject: + Added delObj function to object class --- app/class/object.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index fb38ef7..7f73382 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -158,6 +158,23 @@ abstract class Object extends Framework } } } + + /* + * Remove this object from the database + */ + function delObj() + { + if (!isset($this->guid)) + return; + + /* Delete Base */ + $query = "DELETE FROM `" . $this->table . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + + /* Delete Child */ + $query = "DELETE FROM `" . $this->childTable . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + } } ?> -- cgit v1.2.3 From 30c2345e1567832cbaeefcf4db1e559a8a198046 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 01:52:39 -0500 Subject: * Defined some default values for function parameters for object class -- planning to make a class "RawObject" so that objects may be created in a polymorphic way --- app/class/object.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 7f73382..3622d6a 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -10,7 +10,7 @@ abstract class Object extends Framework /* * Constructor */ - function __construct($childTable, $childCols) + function __construct($childTable = "object", $childCols = null) { $this->db = $this->getDbConnection(); @@ -39,7 +39,7 @@ abstract class Object extends Framework /* * Populate this object with data from the DB with a given GUID */ - function loadObj($guid) + function loadObj($guid = null) { if (is_null($guid)) return; -- cgit v1.2.3 From 25947336340ac5bb7f1f9fc762d6e449320069da Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 02:26:00 -0500 Subject: + Added function "isGUID" to object class for checking whether GUIDs exist --- app/class/object.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 3622d6a..fe487bc 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -44,6 +44,9 @@ abstract class Object extends Framework if (is_null($guid)) return; + if (!$this->isGUID($guid)) + return; + $escdGuid = $this->db->esc($guid); /* Base fields */ @@ -175,6 +178,20 @@ abstract class Object extends Framework $query = "DELETE FROM `" . $this->childTable . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; $this->db->query($query); } + + /* + * Check whether given GUID exists + */ + function isGUID($guid) + { + $query = "SELECT `guid` FROM `object` WHERE `guid` = '" . $this->db->esc($guid) . "'"; + $result = $this->db->query($query); + + if (count($result) > 0) + return true; + + return false; + } } ?> -- cgit v1.2.3 From 877eccf539bfd3a365d8658ed63d096a13e57b00 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 13:52:19 -0500 Subject: + Implemented Object::getNewGUID function for Object class --- app/class/object.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index fe487bc..7a46e6e 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -192,6 +192,21 @@ abstract class Object extends Framework return false; } + + /* + * Get a new, unique GUID for a new system object + */ + function getNewGUID() + { + do + { + $sha = hash("sha256", random_bytes(64)); + $guid = substr($sha, 0, 8); + } + while ($this->isGUID($guid)); + + return $guid; + } } ?> -- cgit v1.2.3 From 00de072a6a90259d20426969ff4d84b2e26959ee Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 15:07:41 -0500 Subject: * now using rand() instead of random_bytes for numbers --- app/class/object.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 7a46e6e..bae57ea 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -200,7 +200,7 @@ abstract class Object extends Framework { do { - $sha = hash("sha256", random_bytes(64)); + $sha = hash("sha256", rand()); $guid = substr($sha, 0, 8); } while ($this->isGUID($guid)); -- cgit v1.2.3 From d508dacd1b5b293df5d0e71cad9cfd87d9f33ff7 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 18 Dec 2015 16:24:26 -0500 Subject: + Added DBObject class -- A non-abstract version of Object class --- app/class/object.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index bae57ea..bcd8dfa 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -209,4 +209,19 @@ abstract class Object extends Framework } } +/* + * Concrete Database Object which can be used in a polymorphic way + */ +class DBObject extends Object +{ + /* + * Constructor + */ + function __construct($guid = null) + { + parent::__construct(); + $this->loadObj($guid); + } +} + ?> -- cgit v1.2.3 From 76f208ddcc490280885c3fd2fd2917e6be0b65b5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 30 Dec 2015 20:18:45 -0500 Subject: + Created db table child class for User table --- app/class/user.class.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 app/class/user.class.php (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php new file mode 100644 index 0000000..8ef91ae --- /dev/null +++ b/app/class/user.class.php @@ -0,0 +1,30 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 58dacd8e07d8d8eddf0d5c0a598a5eae27796473 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 30 Dec 2015 20:49:36 -0500 Subject: + Created class file for extern-user table --- app/class/externuser.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/class/externuser.class.php (limited to 'app/class') diff --git a/app/class/externuser.class.php b/app/class/externuser.class.php new file mode 100644 index 0000000..8f5c9d0 --- /dev/null +++ b/app/class/externuser.class.php @@ -0,0 +1,26 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 00b391ab20e1b0d6e65bfe907819bceef3d1ed84 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 30 Dec 2015 22:51:42 -0500 Subject: + Added class file for group table --- app/class/group.class.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/class/group.class.php (limited to 'app/class') diff --git a/app/class/group.class.php b/app/class/group.class.php new file mode 100644 index 0000000..2f3af40 --- /dev/null +++ b/app/class/group.class.php @@ -0,0 +1,20 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 3231db3d578c510d130b1377d3809228a273b118 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 31 Dec 2015 20:13:26 -0500 Subject: + Added class file for Pad table --- app/class/pad.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/class/pad.class.php (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php new file mode 100644 index 0000000..335ef63 --- /dev/null +++ b/app/class/pad.class.php @@ -0,0 +1,26 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 231a312a77f7f774b48fb5700bea780b274ed82a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 31 Dec 2015 23:02:23 -0500 Subject: + Added class file for stage table --- app/class/stage.class.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/class/stage.class.php (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php new file mode 100644 index 0000000..a2dfba5 --- /dev/null +++ b/app/class/stage.class.php @@ -0,0 +1,25 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From c139f96610ba12f5035f1c3a18448a290714d851 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 1 Jan 2016 00:08:50 -0500 Subject: + Added class file for issue table --- app/class/issue.class.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/class/issue.class.php (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php new file mode 100644 index 0000000..90a095c --- /dev/null +++ b/app/class/issue.class.php @@ -0,0 +1,28 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 4f2f824cfef24b572994c9831dd4bf4bbc575dc1 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 1 Jan 2016 14:27:53 -0500 Subject: + Added class file for message table --- app/class/message.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/class/message.class.php (limited to 'app/class') diff --git a/app/class/message.class.php b/app/class/message.class.php new file mode 100644 index 0000000..d39bea8 --- /dev/null +++ b/app/class/message.class.php @@ -0,0 +1,26 @@ +loadObj($guid); + } +} + +?> -- cgit v1.2.3 From 0a92a484554f214d0e499c872807e6b0a3e865ad Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 1 Jan 2016 16:50:22 -0500 Subject: + Added class file for setting table --- app/class/framework.class.php | 2 +- app/class/setting.class.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/class/setting.class.php (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index eea6c25..d1293de 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -50,7 +50,7 @@ abstract class Framework /* * Get or create the app's database connection object (this is a singleton object and dependent on system-level config) */ - function getDbConnection() + static function getDbConnection() { global $_SCROTT; diff --git a/app/class/setting.class.php b/app/class/setting.class.php new file mode 100644 index 0000000..ea5fac3 --- /dev/null +++ b/app/class/setting.class.php @@ -0,0 +1,28 @@ +esc($key); + + $query = "SELECT `value` FROM `setting` WHERE `key` = '" . $escdKey . "'"; + $res = $db->query($query); + + if (count($res) == 0) + return false; + + return $res[0]['value']; + } +} + +?> -- cgit v1.2.3 From 9ce26b55017a24f3cae5c20958f2d612273c2f60 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 26 Jan 2016 21:55:43 -0500 Subject: + Added function to User class to fetch all users from DB * Altered Auth MVC deflt action to return false if no users are found. This way, the Auth controller can automatically present user a page to create an admin account --- app/class/user.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 8ef91ae..6004dc9 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -25,6 +25,22 @@ class User extends Object parent::__construct("user", $cols); $this->loadObj($guid); } + + /* + * Get all users -- ordered by name, ascending + */ + function getAllUsers_orderByName() + { + $query = "SELECT guid FROM `object` WHERE `type` = 'user' ORDER BY name"; + $result = $this->db->query($query); + + $users = array(); + + foreach ($result as $u) + $users[] = new User($u['guid']); + + return $users; + } } ?> -- cgit v1.2.3 From 635ceb4808624ad6676d43e83c1ff5a7d4341d36 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 28 Jan 2016 19:38:28 -0500 Subject: Add admin field to user table User accounts now have a field to denote whether they are site administrators. The first account created during app initial configuration is an admin automatically. --- app/class/user.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 6004dc9..9a87b01 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -17,6 +17,7 @@ class User extends Object "key", "salt", "alias", + "admin", "email", "emailConf", "emailConfKey" -- cgit v1.2.3 From bad5036569b3c572f60dae034c42a8129adc29e5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 30 Jan 2016 18:22:13 -0500 Subject: Handle object timestamps automatically in Object::saveObj() The saveObj() function now initializes and update the timeCreated and timeUpdated fields of objects on its own. A new function, getCurrentTimestamp() (from class Object) is introduced to aid simpler fetching of the date and time --- app/class/object.class.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index bcd8dfa..93b52f0 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -77,6 +77,8 @@ abstract class Object extends Framework { if (isset($this->guid)) { + $this->timeUpdated = $this->getCurrentTimestamp(); + /* Update Base */ $updateStr = ""; @@ -117,6 +119,8 @@ abstract class Object extends Framework else { $this->guid = $this->getNewGUID(); + $this->timeCreated = $this->getCurrentTimestamp(); + $this->timeUpdated = $this->timeCreated; /* Insert Base */ $colsStr = ""; @@ -179,6 +183,16 @@ abstract class Object extends Framework $this->db->query($query); } + /* + * Get current timestamp for object database purposes + */ + function getCurrentTimestamp() + { + $query = "SELECT now() AS stamp"; + $result = $this->db->query($query); + return $result[0]['stamp']; + } + /* * Check whether given GUID exists */ -- cgit v1.2.3 From b6bb1893ad7b4a901a28b0fa2e725141a7b39509 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 30 Jan 2016 20:48:14 -0500 Subject: Update app source of entropy for creating random blobs Removed use of PHP's rand() functon in favor of openssl extension's openssl_random_pseudo_bytes() to create blobs with better entropy. Created function getBlob (from class Object) to get a sha256 hash created from randomness for use as object GUIDs, password salts, application tokens, etc. --- app/class/object.class.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 93b52f0..96cc810 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -214,13 +214,20 @@ abstract class Object extends Framework { do { - $sha = hash("sha256", rand()); - $guid = substr($sha, 0, 8); + $guid = substr($this->getBlob(), 0, 8); } while ($this->isGUID($guid)); return $guid; } + + /* + * Get a random sha256 blob + */ + function getBlob() + { + return hash("sha256", openssl_random_pseudo_bytes(64)); + } } /* -- cgit v1.2.3 From 2b6afdd9ef767e1e84c4751c72da6be13d9b4402 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 30 Jan 2016 21:20:41 -0500 Subject: Add functionality to create new User objects User class now has a new function which will take a $username and a $password and use it to initialize itself as well as write new object data to the database. This commit introduces a helper function getKey() (from class User) for creating user object keys by hashing the contatenation of its password and salt. This commit introduces a helper function usernameInUse() (from class User) for ensuring the uniqueness of names amongst user-type objects --- app/class/user.class.php | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 9a87b01..6bce26c 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -42,6 +42,60 @@ class User extends Object return $users; } + + /* + * Check whether a given username is currently in use + */ + function usernameInUse($username) + { + $escd_username = $this->db->esc($username); + + $query = "SELECT name FROM object WHERE type = 'user' AND name = '" . $escd_username . "'"; + $results = $this->db->query($query); + + if (count($results) > 0) + return true; + + return false; + } + + /* + * Generate a key from a user's password and salt + */ + function getKey($password, $salt) + { + return hash("sha256", $salt . $password); + } + + /* + * Create a new User object with the given username and keyed with the given plain-text password + * This function returns false if $username is already being used + * On success, this object should be initialized as the new user (use only on new User() objects) + */ + function createNewUser($username, $password) + { + if ($this->usernameInUse($username)) + return false; + + /* if there exist no users already, make this new one an admin */ + if (count($this->getAllUsers_orderByName()) == 0) + $this->admin = 1; + + $this->perms = 0; + $this->name = $username; + $this->type = "user"; + $this->salt = $this->getBlob(); + $this->key = $this->getKey($password, $this->salt); + $this->emailConf = 0; + $this->emailConfKey = $this->getBlob(); + + $this->saveObj(); + + $this->owner = $this->guid; + $this->saveObj(); + + return true; + } } ?> -- cgit v1.2.3 From e15599108f64bd816eb32f8028a81e3db76c19ff Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 31 Jan 2016 16:52:52 -0500 Subject: Implement PHP session semantics in Framework class Added PHP session handling to core framework. Functions now exist to set the current user, get the current user, and get the IP address used to login (to compare with furure requests on the same session to combat session hijacking). --- app/class/framework.class.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index d1293de..74c4b14 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -4,7 +4,11 @@ is_file("scrott.conf.php") && require_once "scrott.conf.php"; +/* Init PHP session */ +session_start(); + require_once "class/mysql.class.php"; +require_once "class/user.class.php"; /* * Global functions / operations and access to contextual or session-based information @@ -47,6 +51,43 @@ abstract class Framework exit; } + /* + * Get a user object for the currently logged in user. Returns false if session is logged out. + */ + function getCurrentUser() + { + if (isset($_SESSION['userguid'])) + return new User($_SESSION['userguid']); + + return false; + } + + /* + * Get the IP address the client held when the current session began + */ + function getOriginIP() + { + return $_SESSION['userip']; + } + + /* + * Set the current logged in user + */ + function setCurrentUser($user = null) + { + if ($user != null && isset($user->guid)) + { + $_SESSION['userguid'] = $user->guid; + $_SESSION['userip'] = $_SERVER['REMOTE_ADDR']; + } + + else + { + unset($_SESSION['userguid']); + unset($_SESSION['userip']); + } + } + /* * Get or create the app's database connection object (this is a singleton object and dependent on system-level config) */ -- cgit v1.2.3 From c776b36fd884808435dd1208f0dd9a57216b3927 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 1 Feb 2016 19:18:55 -0500 Subject: Implement authentication helper functions in User class Added function to initialize a User object by username wrather than GUID. Added function to validate a user-supplied plain-text password for a given user --- app/class/user.class.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 6bce26c..bd2e174 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -27,6 +27,21 @@ class User extends Object $this->loadObj($guid); } + /* + * Initialize object by username + */ + function initByUsername($username) + { + $query = "SELECT guid FROM object WHERE type = 'user' AND name = '" . $this->db->esc($username) . "'"; + $result = $this->db->query($query); + + if (count($result) == 0) + return false; + + $this->loadObj($result[0]['guid']); + return true; + } + /* * Get all users -- ordered by name, ascending */ @@ -96,6 +111,15 @@ class User extends Object return true; } + + /* + * Validate the password for this user. Returns true if correct, false otherwise + */ + function validatePassword($password) + { + $key = $this->getKey($password, $this->salt); + return $key == $this->key; + } } ?> -- cgit v1.2.3 From 7d484a70f73bd679e0dcf18d23d8124d8edf8f63 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 2 Feb 2016 19:52:07 -0500 Subject: Add helper function to Setting class Added a static helper function to replacing (or inserting) an option value in the database, longhand. --- app/class/setting.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/setting.class.php b/app/class/setting.class.php index ea5fac3..b48f241 100644 --- a/app/class/setting.class.php +++ b/app/class/setting.class.php @@ -23,6 +23,23 @@ class Setting extends Framework return $res[0]['value']; } + + /* + * Helper function for setting setting values on the database + */ + static function setValue($key, $value) + { + $db = parent::getDbConnection(); + $escdKey = $db->esc($key); + $escdValue = $db->esc($value); + + if (self::getValue($key) === false) + $query = "INSERT INTO setting (`key`, value) VALUES('" . $escdKey . "', '" . $escdValue . "')"; + else + $query = "UPDATE setting SET value = '" . $escdValue . "' WHERE `key` = '" . $escdKey . "'"; + + $db->query($query); + } } ?> -- cgit v1.2.3 From 4496b56e3392ba8183c0e1764557d51a8633e7ca Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 2 Feb 2016 20:31:29 -0500 Subject: Add admin setting 'allowPublicSignup' This setting will be used to decide if the app should allow unauthenticated users to create their own user accounts or if an admin must create them. --- app/class/setting.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'app/class') diff --git a/app/class/setting.class.php b/app/class/setting.class.php index b48f241..e3ef7f1 100644 --- a/app/class/setting.class.php +++ b/app/class/setting.class.php @@ -40,6 +40,19 @@ class Setting extends Framework $db->query($query); } + + /* + * Should the app allow the public to signup their own accounts with Scrott? + */ + static function allowPublicSignup($value = null) + { + $opt = "allowPublicSignup"; + + if ($value != null) + self::setValue($opt, $value); + + return self::getValue($opt); + } } ?> -- cgit v1.2.3 From f7e765480572920ba396a747f0294c75ab813202 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 7 Feb 2016 15:11:25 -0500 Subject: Add fields to Issue object This adds attributes to an issue: due date (optional datetime) tags (space separated string of words to help categorize issues (again, optional)) --- app/class/issue.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 90a095c..67bf683 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -17,7 +17,9 @@ class Issue extends Object "number", "assignee", "unread", - "desc" + "desc", + "due", + "tags" ); parent::__construct("issue", $cols); -- cgit v1.2.3 From db6505607b2356e91fe396badf50243407f3345b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 7 Feb 2016 16:13:11 -0500 Subject: Add in-app administrative setting: settSSL This is the in-app version of $_SCROTT['settSSL'] system-level setting. Setting::settSSL() overrides $_SCROTT['settSSL'] only if the latter is set to 'neither'. If both are set to 'neither', the app will run on either HTTP or HTTPS depending on how the page was requested. --- app/class/setting.class.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'app/class') diff --git a/app/class/setting.class.php b/app/class/setting.class.php index e3ef7f1..9cafdfd 100644 --- a/app/class/setting.class.php +++ b/app/class/setting.class.php @@ -41,6 +41,24 @@ class Setting extends Framework $db->query($query); } + /* + * Force or forbid SSL connections? + */ + static function settSSL($value = null) + { + $opt = "settSSL"; + + if ($value != null) + self::setValue($opt, $value); + + $value = self::getValue($opt); + + if ($value === false) + return "neither"; + + return $value; + } + /* * Should the app allow the public to signup their own accounts with Scrott? */ -- cgit v1.2.3 From 6252381a2f8c1de374a2ad35d20bc10393d6f47a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 1 Mar 2016 19:50:17 -0500 Subject: Add garbage collection logic to Object::delObj() Now, on deletion of objects, all refs to it are purged from the xref tables, obj_member and msg_read --- app/class/object.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 96cc810..bc8a67f 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -181,6 +181,14 @@ abstract class Object extends Framework /* Delete Child */ $query = "DELETE FROM `" . $this->childTable . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; $this->db->query($query); + + /* obj_member garbage collection */ + $query = "DELETE FROM `obj_member` WHERE `guid` = '" . $this->db->esc($this->guid) . "' OR `member` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + + /* msg_read garbage collection */ + $query = "DELETE FROM `msg_read` WHERE `guid` = '" . $this->db->esc($this->guid) . "' OR `user` = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); } /* -- cgit v1.2.3 From a68db47508b74ccd0d7e6f8529a0f98b59dd69e0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 5 Mar 2016 16:48:58 -0500 Subject: Add verify_ip security assertion This assertion will be used app-wide. This asserts that the IP address a client uses to conenct to the app is constant throughout the the session's lifetime. This is to detect any session hijacking. If a session suddenly appears to be comming from a different IP address, the session will be killed. --- app/class/controller.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/controller.class.php b/app/class/controller.class.php index fabd7e7..effaf78 100644 --- a/app/class/controller.class.php +++ b/app/class/controller.class.php @@ -32,6 +32,21 @@ abstract class Controller extends Framework if (isset($_SERVER['HTTPS'])) $this->redirectTo("http://" . $_SERVER['SERVER_NAME'] . $this->ap()); } + + /* + * Security check + * Assert that the client's IP address does not change during its session. If a change is detected, logout. + */ + function sec_verify_ip() + { + $addr = $_SERVER['REMOTE_ADDR']; + + if ($this->getCurrentUser() && $addr != $this->getOriginIP()) + { + $this->setCurrentUser(); + $this->redirectTo($this->ar() . "/"); + } + } } ?> -- cgit v1.2.3 From 85cead8ab00b13abaa7f729052792fc845756857 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 26 Mar 2016 23:56:38 -0400 Subject: Fix bug in Framework::getCurrentUser() function If, by some means, the GUID for a logged in user is not valid, that session should be terminated ("$this->setCurrentUser();") This might happen if the database gets flushed, or if an account gets removed while it is in use... --- app/class/framework.class.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 74c4b14..4223d68 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -57,7 +57,14 @@ abstract class Framework function getCurrentUser() { if (isset($_SESSION['userguid'])) - return new User($_SESSION['userguid']); + { + $user = new User($_SESSION['userguid']); + + if ($user->type == "user") + return $user; + + $this->setCurrentUser(); + } return false; } -- cgit v1.2.3 From 7df5bfb84ac979e26cd23042bc8bbcf53d3f6ae6 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 6 Mar 2016 14:22:50 -0500 Subject: Add function getDisplayName() to User class If a user has an alias set, it should be displayed throughout the app instead of the username. --- app/class/user.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index bd2e174..4f1bbfe 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -120,6 +120,17 @@ class User extends Object $key = $this->getKey($password, $this->salt); return $key == $this->key; } + + /* + * If a user has an alias set, display it instead of their username + */ + function getDisplayName() + { + if ($this->alias != "") + return $this->alias; + + return $this->name; + } } ?> -- cgit v1.2.3 From 798cd5d80385705503c81be269c008e163fcbdba Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 10 Mar 2016 18:36:46 -0500 Subject: Add function User::setPassword() This user function will update the salt and key for a user object to change its password. --- app/class/user.class.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 4f1bbfe..75b769a 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -99,8 +99,7 @@ class User extends Object $this->perms = 0; $this->name = $username; $this->type = "user"; - $this->salt = $this->getBlob(); - $this->key = $this->getKey($password, $this->salt); + $this->setPassword($password); $this->emailConf = 0; $this->emailConfKey = $this->getBlob(); @@ -121,6 +120,15 @@ class User extends Object return $key == $this->key; } + /* + * Overwrite the salt and key for this user, given a new plaintext password + */ + function setPassword($password) + { + $this->salt = $this->getBlob(); + $this->key = $this->getKey($password, $this->salt); + } + /* * If a user has an alias set, display it instead of their username */ -- cgit v1.2.3 From 2b8f6ae7ca3e8b18f80f2753d990425aa7fac820 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 10 Mar 2016 19:09:50 -0500 Subject: Add function User::setEmail() This function handles internal vars while updating a user's email address. --- app/class/user.class.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 75b769a..6c8f46f 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -100,8 +100,7 @@ class User extends Object $this->name = $username; $this->type = "user"; $this->setPassword($password); - $this->emailConf = 0; - $this->emailConfKey = $this->getBlob(); + $this->setEmail(""); $this->saveObj(); @@ -129,6 +128,16 @@ class User extends Object $this->key = $this->getKey($password, $this->salt); } + /* + * Overwrite the emailConfKey and flag, and change user's saved email address + */ + function setEmail($email) + { + $this->email = $email; + $this->emailConf = 0; + $this->emailConfKey = $this->getBlob(); + } + /* * If a user has an alias set, display it instead of their username */ -- cgit v1.2.3 From f270bc774776dd5733ed4d76095281fa9210bac2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 10 Mar 2016 19:18:19 -0500 Subject: Add function User::confirmEmailKey() Validates the users supposed email key. If correct, sets the users emailConf flag. --- app/class/user.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 6c8f46f..7d8519e 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -119,6 +119,18 @@ class User extends Object return $key == $this->key; } + /* + * Validate the email confirmation key for a user, returns true if correct, false otherwise. On success, $this->emailConf is also set to 1 + */ + function confirmEmailKey($key) + { + if ($key != $this->emailConfKey) + return false; + + $this->emailConf = 1; + return true; + } + /* * Overwrite the salt and key for this user, given a new plaintext password */ -- cgit v1.2.3 From cc755e3756e43109d0db0de963b3a132039456b1 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 12 Mar 2016 15:05:12 -0500 Subject: Alter representation of form boolean values Changed how Form() objects model true and false for boolean fields. Was "true" and "false", is now "1" and "0", respectivly. This is to address how Mysql handles these values as they are pushed to the db. --- app/class/form.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 808de27..9f103ba 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -82,7 +82,7 @@ class Form */ function field_bool($name) { - $this->field_enum($name, array("true", "false"), "false"); + $this->field_enum($name, array("1", "0"), "0"); } /* -- cgit v1.2.3 From 2936f0d151fb52bd2649edc37abd2e1d559d1f0f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 12 Mar 2016 23:38:18 -0500 Subject: Move getUserGlyphicon function from common model into user class --- app/class/user.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 7d8519e..f1f7ff1 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -160,6 +160,17 @@ class User extends Object return $this->name; } + + /* + * Get the glyphicon to use for this user + */ + function getGlyphicon() + { + if ($this->admin) + return "glyphicon glyphicon-sunglasses"; + + return "glyphicon glyphicon-user"; + } } ?> -- cgit v1.2.3 From 333351cbd18d12520fb0eae44e9805cb3b10e038 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 13 Mar 2016 23:56:40 -0400 Subject: Add function User::getAddUsers_orderByAdminByName() Added function to retrive all users in system presorted first by admin status (Admins first), then by username in alpha order --- app/class/user.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index f1f7ff1..1130396 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -58,6 +58,22 @@ class User extends Object return $users; } + /* + * Get all users -- ordered by admin DESC (admins first), then by name + */ + function getAllUsers_orderByAdminByName() + { + $query = "SELECT o.guid FROM object o JOIN user u ON o.guid = u.guid WHERE o.type = 'user' ORDER BY u.admin DESC, o.name"; + $result = $this->db->query($query); + + $users = array(); + + foreach ($result as $u) + $users[] = new User($u['guid']); + + return $users; + } + /* * Check whether a given username is currently in use */ -- cgit v1.2.3 From 8ad6e8f9223bd3ee214478b3e1247f9c7d8e91ec Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 27 Mar 2016 22:32:25 -0400 Subject: Add form field type 'file' Add the Form::field_file() function to allow form handlers to specify they expect to receive file from the end-user. This adds data about the file field to the form, but does not yet handle it in the populate function --- app/class/form.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 9f103ba..e748afc 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -13,6 +13,7 @@ class Form $this->textFields = array(); $this->numbFields = array(); $this->enumFields = array(); + $this->fileFields = array(); $this->errorlist = array(); } @@ -85,6 +86,22 @@ class Form $this->field_enum($name, array("1", "0"), "0"); } + /* + * Add new file field to the form + */ + function field_file($name, $maxsize, $allowed_mime = null, $req = false) + { + if ($req !== true) + $req = false; + + $this->fileFields[] = array( + 'name' => $name, + 'maxsize' => $maxsize, + 'mime' => $allowed_mime, + 'req' => $req + ); + } + /* * Populate the form with input data from web page */ -- cgit v1.2.3 From 1f8b53e426b8c0a1546e9d5c21573be9003cb556 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 27 Mar 2016 23:59:08 -0400 Subject: Update function Form::populate() to initialize fields added with Form::field_file() Set $form->[name] for each file field type setup on the form. --- app/class/form.class.php | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index e748afc..529c480 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -105,7 +105,7 @@ class Form /* * Populate the form with input data from web page */ - function populate($input) + function populate($input, $files = null) { /* detect duplicate names */ $names = array(); @@ -115,6 +115,8 @@ class Form $names[] = $fld['name']; foreach ($this->enumFields as $fld) $names[] = $fld['name']; + foreach ($this->fileFields as $fld) + $names[] = $fld['name']; if (count(array_unique($names)) != count($names)) { @@ -195,6 +197,38 @@ class Form $this->logError($fld['name'] . " is required"); } + /* init file fields */ + foreach ($this->fileFields as $fld) + { + if (!is_null($files) && isset($files[$fld['name']])) + { + $file = $files[$fld['name']]; + + if ($file['error'] > 0) + { + $this->logError("An unknown error occurred"); + continue; + } + + if ($file['size'] > $fld['maxsize']) + { + $this->logError("File must be no larger than " . $fld['maxsize'] . " bytes"); + continue; + } + + if (is_array($fld['mime']) && array_search($file['type'], $fld['mime']) === false) + { + $this->logError("File type is not supported"); + continue; + } + + $this->$fld['name'] = $file; + } + + else if ($fld['req']) + $this->logError($fld['name'] . " is required"); + } + /* return */ return count($this->errorlist) == 0; } -- cgit v1.2.3 From 3d493fc75dc6e3593001c2d9dfef26f4c1d79c2c Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 29 Mar 2016 19:59:36 -0400 Subject: Add function Form::saveUploadedFile() Added function to form class to move tmp uploaded files to permanent storage --- app/class/form.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 529c480..3f28a36 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -232,6 +232,14 @@ class Form /* return */ return count($this->errorlist) == 0; } + + /* + * Save file uploaded through web form + */ + function saveUploadedFile($file, $filename) + { + return move_uploaded_file($file['tmp_name'], $filename); + } } ?> -- cgit v1.2.3 From dbf4512ce48c04af4a8332cff3064f29b5690446 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 27 Apr 2016 20:41:35 -0400 Subject: Revert "Add function Form::saveUploadedFile()" This reverts commit 3d493fc75dc6e3593001c2d9dfef26f4c1d79c2c. The way I was wanting to handle file uploads isn't going to fly with a semantic of PHP and POST var mgmt. -.- Rolling back relevant changes to write up something else. --- app/class/form.class.php | 8 -------- 1 file changed, 8 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 3f28a36..529c480 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -232,14 +232,6 @@ class Form /* return */ return count($this->errorlist) == 0; } - - /* - * Save file uploaded through web form - */ - function saveUploadedFile($file, $filename) - { - return move_uploaded_file($file['tmp_name'], $filename); - } } ?> -- cgit v1.2.3 From caebcb442985496b3bddaa3e7c1a752832e32e0b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 27 Apr 2016 20:43:35 -0400 Subject: Revert "Update function Form::populate() to initialize fields added with Form::field_file()" This reverts commit 1f8b53e426b8c0a1546e9d5c21573be9003cb556. See parent commit message. --- app/class/form.class.php | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 529c480..e748afc 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -105,7 +105,7 @@ class Form /* * Populate the form with input data from web page */ - function populate($input, $files = null) + function populate($input) { /* detect duplicate names */ $names = array(); @@ -115,8 +115,6 @@ class Form $names[] = $fld['name']; foreach ($this->enumFields as $fld) $names[] = $fld['name']; - foreach ($this->fileFields as $fld) - $names[] = $fld['name']; if (count(array_unique($names)) != count($names)) { @@ -197,38 +195,6 @@ class Form $this->logError($fld['name'] . " is required"); } - /* init file fields */ - foreach ($this->fileFields as $fld) - { - if (!is_null($files) && isset($files[$fld['name']])) - { - $file = $files[$fld['name']]; - - if ($file['error'] > 0) - { - $this->logError("An unknown error occurred"); - continue; - } - - if ($file['size'] > $fld['maxsize']) - { - $this->logError("File must be no larger than " . $fld['maxsize'] . " bytes"); - continue; - } - - if (is_array($fld['mime']) && array_search($file['type'], $fld['mime']) === false) - { - $this->logError("File type is not supported"); - continue; - } - - $this->$fld['name'] = $file; - } - - else if ($fld['req']) - $this->logError($fld['name'] . " is required"); - } - /* return */ return count($this->errorlist) == 0; } -- cgit v1.2.3 From 9458874e8c194b1c5a53bd3e85f9ba7548c9dac4 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 27 Apr 2016 20:47:21 -0400 Subject: Revert "Add form field type 'file'" This reverts commit 8ad6e8f9223bd3ee214478b3e1247f9c7d8e91ec. See parent commit message --- app/class/form.class.php | 17 ----------------- 1 file changed, 17 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index e748afc..9f103ba 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -13,7 +13,6 @@ class Form $this->textFields = array(); $this->numbFields = array(); $this->enumFields = array(); - $this->fileFields = array(); $this->errorlist = array(); } @@ -86,22 +85,6 @@ class Form $this->field_enum($name, array("1", "0"), "0"); } - /* - * Add new file field to the form - */ - function field_file($name, $maxsize, $allowed_mime = null, $req = false) - { - if ($req !== true) - $req = false; - - $this->fileFields[] = array( - 'name' => $name, - 'maxsize' => $maxsize, - 'mime' => $allowed_mime, - 'req' => $req - ); - } - /* * Populate the form with input data from web page */ -- cgit v1.2.3 From db369507f636f9395604a5cd72fcfe5d99f97166 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 27 Apr 2016 22:11:58 -0400 Subject: Add function Form::saveFile() This is a rework of how the framework handles performing file uploads. Rather than attaching new fields to a form (of type file) and handling them during populate() then saving later, users can call what is essentially a static function and save files in isolation. Since each webform I can conceive using in Scrott at this time won't be uploading more than one file at a time, this model should work nicely moving forward; however can be easily adjusted if need be. --- app/class/form.class.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 9f103ba..907c0a2 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -181,6 +181,47 @@ class Form /* return */ return count($this->errorlist) == 0; } + + /* + * Handle an uploaded file + */ + function saveFile($file, $maxsize, $allowed_mime, $path, $req = false) + { + if (isset($file) && !is_null($file)) + { + if ($file['error'] > 0) + { + $this->logError("An unknown error occurred"); + return false; + } + + if ($file['size'] > $maxsize) + { + $this->logError("File must be no larger than " . $maxsize . " bytes"); + return false; + } + + if (is_array($allowed_mime) && array_search($file['type'], $allowed_mime) === false) + { + $this->logError("File type is not supported"); + return false; + } + + if (!move_uploaded_file($file['tmp_name'], $path)) + { + $this->logError("Error saving uploaded file"); + return false; + } + } + + else if ($req) + { + $this->logError("File upload is required"); + return false; + } + + return true; + } } ?> -- cgit v1.2.3 From ac5410de94127d7fe139de0e9bf4fd1c20bdae2b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 1 Apr 2016 00:46:17 -0400 Subject: Add function getHeadImage() to User class This function will return the path to the head image (user image) for the user object. This path should be something like: /file.php?d=img/heads&f= --- app/class/user.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 1130396..ab9ecf5 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -187,6 +187,14 @@ class User extends Object return "glyphicon glyphicon-user"; } + + /* + * Get this user's head image + */ + function getHeadImage() + { + return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; + } } ?> -- cgit v1.2.3 From ed2af3a874417b8317bb6def9b40189b1d5d05e8 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 30 Apr 2016 16:57:11 -0400 Subject: Fix to Form::saveFile() Only log an error if we get an upload error besides err code 4 (No file uploaded) --- app/class/form.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 907c0a2..fd85638 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -191,7 +191,9 @@ class Form { if ($file['error'] > 0) { - $this->logError("An unknown error occurred"); + if ($file['error'] != UPLOAD_ERR_NO_FILE) + $this->logError("An unknown error occurred"); + return false; } -- cgit v1.2.3 From 6d6e62add5976b3afec23d1745302d676ccd88ac Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 30 Apr 2016 22:19:36 -0400 Subject: Add function User::rmHeadImage() Function to delete the user image file for the given user object. --- app/class/user.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index ab9ecf5..1d17dfe 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -195,6 +195,17 @@ class User extends Object { return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; } + + /* + * Remove this user's head image + */ + function rmHeadImage() + { + if (!is_file("assets/img/heads/" . $this->guid)) + return true; + + return unlink("assets/img/heads/" . $this->guid); + } } ?> -- cgit v1.2.3 From 5f99922eb6fbda82da55ccf728eda6add48cb4f1 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 22 May 2016 02:50:17 -0400 Subject: Add function User::getNumAdmins() Function to count the number of admin accounts that exist. This is used to make sure that while deleteing accounts, the number of administrators never drops to zero. --- app/class/user.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 1d17dfe..07bd0d6 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -74,6 +74,16 @@ class User extends Object return $users; } + /* + * Get the number of administrative accounts in the system + */ + function getNumAdmins() + { + $query = "SELECT count(*) as cnt FROM user WHERE admin = 1"; + $results = $this->db->query($query); + return $results[0]['cnt']; + } + /* * Check whether a given username is currently in use */ -- cgit v1.2.3 From 37f5031ab0d53f85dd411e868cf83a0ab4439246 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 22 May 2016 17:29:16 -0400 Subject: Define Scrott version number constant throughout app --- app/class/framework.class.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app/class') diff --git a/app/class/framework.class.php b/app/class/framework.class.php index 4223d68..c773c5e 100644 --- a/app/class/framework.class.php +++ b/app/class/framework.class.php @@ -1,5 +1,8 @@ Date: Thu, 26 May 2016 23:46:22 -0400 Subject: Add copyright notice to Scrott class files --- app/class/controller.class.php | 14 ++++++++++++++ app/class/database.iface.php | 14 ++++++++++++++ app/class/externuser.class.php | 14 ++++++++++++++ app/class/form.class.php | 14 ++++++++++++++ app/class/framework.class.php | 14 ++++++++++++++ app/class/group.class.php | 14 ++++++++++++++ app/class/issue.class.php | 14 ++++++++++++++ app/class/message.class.php | 14 ++++++++++++++ app/class/model.class.php | 14 ++++++++++++++ app/class/mysql.class.php | 14 ++++++++++++++ app/class/object.class.php | 14 ++++++++++++++ app/class/pad.class.php | 14 ++++++++++++++ app/class/setting.class.php | 14 ++++++++++++++ app/class/stage.class.php | 14 ++++++++++++++ app/class/user.class.php | 14 ++++++++++++++ 15 files changed, 210 insertions(+) (limited to 'app/class') diff --git a/app/class/controller.class.php b/app/class/controller.class.php index effaf78..0ab1a69 100644 --- a/app/class/controller.class.php +++ b/app/class/controller.class.php @@ -1,5 +1,19 @@ Date: Wed, 1 Jun 2016 21:59:02 -0400 Subject: Add function Group::createNewGroup() This function will initialize a new group object and write it to the database, with a given group name and owner user. --- app/class/group.class.php | 15 +++++++++++++++ app/class/object.class.php | 2 ++ 2 files changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/group.class.php b/app/class/group.class.php index dfa7deb..246276a 100644 --- a/app/class/group.class.php +++ b/app/class/group.class.php @@ -29,6 +29,21 @@ class Group extends Object parent::__construct(); $this->loadObj($guid); } + + /* + * Create a new user group object. + * On success, this object should be initialized as the new group (use only on new + * Group() objects) + */ + function createNewGroup($name, $owner) + { + $this->perms = $this->DEFAULT_OBJECT_PERMISSIONS; + $this->owner = $owner->guid; + $this->name = $name; + $this->type = "group"; + + $this->saveObj(); + } } ?> diff --git a/app/class/object.class.php b/app/class/object.class.php index b73a54d..42c9355 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -21,6 +21,8 @@ require_once "class/framework.class.php"; */ abstract class Object extends Framework { + var $DEFAULT_OBJECT_PERMISSIONS = 120; + /* * Constructor */ -- cgit v1.2.3 From d70f5ac0ddc976fff9a526996dca8ea6e69d9a16 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 8 Jun 2016 23:23:04 -0400 Subject: Add function Object::getMembers() Added object function to get an array of all its members. These will always be user objects, so this is always a safe function to call. --- app/class/object.class.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 42c9355..a64bdfb 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -252,6 +252,22 @@ abstract class Object extends Framework { return hash("sha256", openssl_random_pseudo_bytes(64)); } + + /* + * Get an array of all members of this object + */ + function getMembers() + { + $query = "SELECT member FROM obj_member WHERE guid = '" . $this->db->esc($this->guid) . "'"; + $result = $this->db->query($query); + + $members = array(); + + foreach ($result as $m) + $members[] = new User($m['member']); + + return $members; + } } /* -- cgit v1.2.3 From 3f58919204a6d21111b6ff00ccec1e2a9dfac040 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 8 Jun 2016 23:36:35 -0400 Subject: Add function Object::getOwner() Added object function to get the owner of an object. This base-class function returns a User object, however a user might not always be the kind of owner (eg: a group can own a pad). In these situations, Object sub-classes should override this function and return the appropriate type of object. --- app/class/object.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index a64bdfb..8a4b956 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -15,6 +15,7 @@ */ require_once "class/framework.class.php"; +require_once "class/user.class.php"; /* * Base class for Scrott database objects @@ -253,6 +254,17 @@ abstract class Object extends Framework return hash("sha256", openssl_random_pseudo_bytes(64)); } + /* + * Get a user object for this object's owner + */ + function getOwner() + { + if (isset($this->owner)) + return new User($this->owner); + + return null; + } + /* * Get an array of all members of this object */ -- cgit v1.2.3 From 3e05bd0357d1cecc89c865a8b339b114b5b91f67 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 10 Jun 2016 00:54:12 -0400 Subject: Add functions to Object class to determine user permissions Added a variety of functions to the Object base class for testing a user's access level to another object. Also added functions to test whether a given user or group is an owner or member of another object. --- app/class/object.class.php | 313 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 8a4b956..a409fa9 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -280,6 +280,319 @@ abstract class Object extends Framework return $members; } + + /* + * Check if given user (or group) is the owner if this object + */ + function isOwner($ug) + { + return $this->getOwner()->guid == $ug->guid; + } + + /* + * Check if given user (or group) is a member of this object + */ + function isMember($ug) + { + foreach ($this->getMembers() as $member) + { + if ($member->guid == $ug->guid) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canAccess($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user)) + return true; + + if ($this->perms & 0x004) // accessible by public + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canAccessSub($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canAccessSub($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModify($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x100) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySub($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySub($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModifyMembers($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x080) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySubMembers($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySubMembers($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModifyPermissions($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySubPermissions($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySubPermissions($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canAccessSub($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x040) + return true; + + if ($this->perms & 0x002) // accessible by public + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canAccessSub($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canAccessSub($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canCreateSub($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x020) + return true; + + if ($this->perms & 0x001) // accessible by public + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canCreateSub($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canCreateSub($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModifySub($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x010) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySub($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySub($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModifySubMembers($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->isMember($user) && $this->perms & 0x008) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySubMembers($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySubMembers($user)) + return true; + } + + return false; + } + + /* + * Check if given user has permissions for this object + */ + function canModifySubPermissions($user) + { + if ($user->admin) + return true; + + if ($this->isOwner($user)) + return true; + + if ($this->parent != "") + { + $parent = new DBObject($this->parent); + + if ($parent->canModifySubPermissions($user)) + return true; + } + else if ($this->owner != $this->guid) + { + $owner = new DBObject($this->owner); + + if ($owner->canModifySubPermissions($user)) + return true; + } + + return false; + } } /* -- cgit v1.2.3 From e2328c50f6fd101b4eaee410afb23290965b45b9 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 10 Jun 2016 22:45:55 -0400 Subject: Fix typo --- app/class/object.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index a409fa9..cfc452c 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -282,7 +282,7 @@ abstract class Object extends Framework } /* - * Check if given user (or group) is the owner if this object + * Check if given user (or group) is the owner of this object */ function isOwner($ug) { -- cgit v1.2.3 From 78667b156328dfe500330aa3bf83bf84e3116948 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 11 Jun 2016 01:11:23 -0400 Subject: Add function User::getGroups() This function returns all groups the user either owns or is a member of. This is not necessarily the same as all groups the user has access permission to. The *not-yet-implemented* object explorer feature should be used to browse those. --- app/class/user.class.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 1185f45..3239568 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -15,6 +15,7 @@ */ require_once "class/object.class.php"; +require_once "class/group.class.php"; /* * Application users @@ -230,6 +231,30 @@ class User extends Object return unlink("assets/img/heads/" . $this->guid); } + + /* + * Get all groups this user owns or is a member of + */ + function getGroups() + { + /* owner */ + $query = "SELECT guid FROM object WHERE type = 'group' AND owner = '" . $this->db->esc($this->guid) . "'"; + $result = $this->db->query($query); + + $groups = array(); + + foreach ($result as $g) + $groups[] = new Group($g['guid']); + + /* member */ + $query = "SELECT o.guid FROM object o JOIN obj_member om ON o.guid = om.guid WHERE o.type = 'group' AND member = '" . $this->db->esc($this->guid) . "'"; + $result = $this->db->query($query); + + foreach ($result as $g) + $groups[] = new Group($g['guid']); + + return $groups; + } } ?> -- cgit v1.2.3 From 63e56817123810e93a3d5cd0e13a70b8c47cacc5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Sep 2016 20:34:15 -0400 Subject: Changes to the handling of indirect variables, properties, and methods To maintain forward compatability with newer versions of PHP (and since my dev environment is now running PHP 7), this patch is made to address the following breaking change from PHP 5: PHP 7 now uses an abstract syntax tree when parsing source files. This has permitted many improvements to the language which were previously impossible due to limitations in the parser used in earlier versions of PHP, but has resulted in the removal of a few special cases for consistency reasons, which has resulted in backward compatibility breaks. Indirect access to variables, properties, and methods will now be evaluated strictly in left-to-right order, as opposed to the previous mix of special cases. --- app/class/form.class.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index f0d660a..8bb6506 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -123,10 +123,10 @@ class Form foreach ($this->textFields as $fld) { if (isset($input[$fld['name']]) && $input[$fld['name']] != "") - $this->$fld['name'] = htmlEntities($input[$fld['name']], ENT_QUOTES); + $this->{$fld['name']} = htmlEntities($input[$fld['name']], ENT_QUOTES); else if (!is_null($fld['deflt'])) - $this->$fld['name'] = $fld['deflt']; + $this->{$fld['name']} = $fld['deflt']; else if ($fld['req']) $this->logError($fld['name'] . " is required"); @@ -161,11 +161,11 @@ class Form continue; } - $this->$fld['name'] = $input[$fld['name']]; + $this->{$fld['name']} = $input[$fld['name']]; } else if (!is_null($fld['deflt'])) - $this->$fld['name'] = $fld['deflt']; + $this->{$fld['name']} = $fld['deflt']; else if ($fld['req']) $this->logError($fld['name'] . " is required"); @@ -182,11 +182,11 @@ class Form continue; } - $this->$fld['name'] = $input[$fld['name']]; + $this->{$fld['name']} = $input[$fld['name']]; } else if (!is_null($fld['deflt'])) - $this->$fld['name'] = $fld['deflt']; + $this->{$fld['name']} = $fld['deflt']; else if ($fld['req']) $this->logError($fld['name'] . " is required"); -- cgit v1.2.3 From 7fd20cd4e15aec3079377e48f18ba91bbda462eb Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Sep 2016 23:32:37 -0400 Subject: Move function User::getHeadImage() to Object class Increase the scope of this function so it may be used by groups. --- app/class/object.class.php | 8 ++++++++ app/class/user.class.php | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index cfc452c..8f64fc4 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -593,6 +593,14 @@ abstract class Object extends Framework return false; } + + /* + * Get object's head image + */ + function getHeadImage() + { + return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; + } } /* diff --git a/app/class/user.class.php b/app/class/user.class.php index 3239568..44b4b5f 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -213,14 +213,6 @@ class User extends Object return "glyphicon glyphicon-user"; } - /* - * Get this user's head image - */ - function getHeadImage() - { - return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; - } - /* * Remove this user's head image */ -- cgit v1.2.3 From 3c3cde4afc1e590063ca72b10ffe566d7fd690d2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 17 Sep 2016 19:06:41 -0400 Subject: Add function Object::getURL() --- app/class/object.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 8f64fc4..1a01ada 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -594,6 +594,14 @@ abstract class Object extends Framework return false; } + /* + * Get URL to this object + */ + function getURL() + { + return $this->ar() . "/" . $this->guid; + } + /* * Get object's head image */ -- cgit v1.2.3 From 35da301d31045b0974100307a7f0f4128b482170 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 18 Sep 2016 11:59:11 -0400 Subject: Move function User::rmHeadImage() to Object class --- app/class/object.class.php | 11 +++++++++++ app/class/user.class.php | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 1a01ada..7c0b7bb 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -609,6 +609,17 @@ abstract class Object extends Framework { return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; } + + /* + * Remove this object's head image + */ + function rmHeadImage() + { + if (!is_file("assets/img/heads/" . $this->guid)) + return true; + + return unlink("assets/img/heads/" . $this->guid); + } } /* diff --git a/app/class/user.class.php b/app/class/user.class.php index 44b4b5f..b8143a9 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -213,17 +213,6 @@ class User extends Object return "glyphicon glyphicon-user"; } - /* - * Remove this user's head image - */ - function rmHeadImage() - { - if (!is_file("assets/img/heads/" . $this->guid)) - return true; - - return unlink("assets/img/heads/" . $this->guid); - } - /* * Get all groups this user owns or is a member of */ -- cgit v1.2.3 From ed99654d2e139a847a63e9295bf976d17462ee34 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 22 Oct 2016 00:29:30 -0400 Subject: Deprecate application code Setup to perform an iteration of development focused on a simpler implementation and eliminating redundancy in design. --- app/class/controller.class.php | 66 ----- app/class/database.iface.php | 27 -- app/class/externuser.class.php | 40 --- app/class/form.class.php | 243 ---------------- app/class/framework.class.php | 144 ---------- app/class/group.class.php | 49 ---- app/class/issue.class.php | 44 --- app/class/message.class.php | 40 --- app/class/model.class.php | 91 ------ app/class/mysql.class.php | 77 ----- app/class/object.class.php | 640 ----------------------------------------- app/class/pad.class.php | 40 --- app/class/setting.class.php | 90 ------ app/class/stage.class.php | 39 --- app/class/user.class.php | 241 ---------------- 15 files changed, 1871 deletions(-) delete mode 100644 app/class/controller.class.php delete mode 100644 app/class/database.iface.php delete mode 100644 app/class/externuser.class.php delete mode 100644 app/class/form.class.php delete mode 100644 app/class/framework.class.php delete mode 100644 app/class/group.class.php delete mode 100644 app/class/issue.class.php delete mode 100644 app/class/message.class.php delete mode 100644 app/class/model.class.php delete mode 100644 app/class/mysql.class.php delete mode 100644 app/class/object.class.php delete mode 100644 app/class/pad.class.php delete mode 100644 app/class/setting.class.php delete mode 100644 app/class/stage.class.php delete mode 100644 app/class/user.class.php (limited to 'app/class') diff --git a/app/class/controller.class.php b/app/class/controller.class.php deleted file mode 100644 index 0ab1a69..0000000 --- a/app/class/controller.class.php +++ /dev/null @@ -1,66 +0,0 @@ -redirectTo("https://" . $_SERVER['SERVER_NAME'] . $this->ap()); - } - - /* - * Security check - * Assert that the current connection to this server is NOT secure. Redirects if not. - */ - function sec_forbid_https() - { - if (isset($_SERVER['HTTPS'])) - $this->redirectTo("http://" . $_SERVER['SERVER_NAME'] . $this->ap()); - } - - /* - * Security check - * Assert that the client's IP address does not change during its session. If a change is detected, logout. - */ - function sec_verify_ip() - { - $addr = $_SERVER['REMOTE_ADDR']; - - if ($this->getCurrentUser() && $addr != $this->getOriginIP()) - { - $this->setCurrentUser(); - $this->redirectTo($this->ar() . "/"); - } - } -} - -?> diff --git a/app/class/database.iface.php b/app/class/database.iface.php deleted file mode 100644 index b1427a4..0000000 --- a/app/class/database.iface.php +++ /dev/null @@ -1,27 +0,0 @@ - diff --git a/app/class/externuser.class.php b/app/class/externuser.class.php deleted file mode 100644 index 73c41bd..0000000 --- a/app/class/externuser.class.php +++ /dev/null @@ -1,40 +0,0 @@ -loadObj($guid); - } -} - -?> diff --git a/app/class/form.class.php b/app/class/form.class.php deleted file mode 100644 index 8bb6506..0000000 --- a/app/class/form.class.php +++ /dev/null @@ -1,243 +0,0 @@ -textFields = array(); - $this->numbFields = array(); - $this->enumFields = array(); - - $this->errorlist = array(); - } - - /* - * Log an error - */ - function logError($str) - { - $this->errorlist[] = $str; - } - - /* - * Add new text field to the form - */ - function field_text($name, $deflt = null, $req = true) - { - if ($req !== true) - $req = false; - - $this->textFields[] = array( - 'name' => $name, - 'deflt' => $deflt, - 'req' => $req - ); - } - - /* - * Add new numeric field to the form - */ - function field_numeric($name, $min = null, $max = null, $deflt = null, $integer = true, $req = true) - { - if ($req !== true) - $req = false; - - if ($integer !== true) - $integer = false; - - $this->numbFields[] = array( - 'name' => $name, - 'min' => $min, - 'max' => $max, - 'deflt' => $deflt, - 'int' => $integer, - 'req' => $req - ); - } - - /* - * Add new enumeration field to the form - */ - function field_enum($name, $values, $deflt = null, $req = true) - { - if ($req !== true) - $req = false; - - $this->enumFields[] = array( - 'name' => $name, - 'vals' => $values, - 'deflt' => $deflt, - 'req' => $req - ); - } - - /* - * Add new boolean field to the form - */ - function field_bool($name) - { - $this->field_enum($name, array("1", "0"), "0"); - } - - /* - * Populate the form with input data from web page - */ - function populate($input) - { - /* detect duplicate names */ - $names = array(); - foreach ($this->textFields as $fld) - $names[] = $fld['name']; - foreach ($this->numbFields as $fld) - $names[] = $fld['name']; - foreach ($this->enumFields as $fld) - $names[] = $fld['name']; - - if (count(array_unique($names)) != count($names)) - { - $this->logError("Internal error: Duplicate field names defined in form"); - return false; - } - - /* init text fields */ - foreach ($this->textFields as $fld) - { - if (isset($input[$fld['name']]) && $input[$fld['name']] != "") - $this->{$fld['name']} = htmlEntities($input[$fld['name']], ENT_QUOTES); - - else if (!is_null($fld['deflt'])) - $this->{$fld['name']} = $fld['deflt']; - - else if ($fld['req']) - $this->logError($fld['name'] . " is required"); - } - - /* init numeric fields */ - foreach ($this->numbFields as $fld) - { - if (isset($input[$fld['name']]) && $input[$fld['name']] != "") - { - if (!is_numeric($input[$fld['name']])) - { - $this->logError($fld['name'] . " must be numeric"); - continue; - } - - if ($fld['int'] && (floor($input[$fld['name']]) != $input[$fld['name']])) - { - $this->logError($fld['name'] . " must be an integer"); - continue; - } - - if (!is_null($fld['min']) && ($input[$fld['name']] < $fld['min'])) - { - $this->logError($fld['name'] . " must be no less than " . $fld['min']); - continue; - } - - if (!is_null($fld['max']) && ($input[$fld['name']] > $fld['max'])) - { - $this->logError($fld['name'] . " must be no more than " . $fld['max']); - continue; - } - - $this->{$fld['name']} = $input[$fld['name']]; - } - - else if (!is_null($fld['deflt'])) - $this->{$fld['name']} = $fld['deflt']; - - else if ($fld['req']) - $this->logError($fld['name'] . " is required"); - } - - /* init enum fields */ - foreach ($this->enumFields as $fld) - { - if (isset($input[$fld['name']]) && $input[$fld['name']] != "") - { - if (array_search($input[$fld['name']], $fld['vals']) === false) - { - $this->logError($fld['name'] . " is not an appropriate value"); - continue; - } - - $this->{$fld['name']} = $input[$fld['name']]; - } - - else if (!is_null($fld['deflt'])) - $this->{$fld['name']} = $fld['deflt']; - - else if ($fld['req']) - $this->logError($fld['name'] . " is required"); - } - - /* return */ - return count($this->errorlist) == 0; - } - - /* - * Handle an uploaded file - */ - function saveFile($file, $maxsize, $allowed_mime, $path, $req = false) - { - if (isset($file) && !is_null($file)) - { - if ($file['error'] > 0) - { - if ($file['error'] != UPLOAD_ERR_NO_FILE) - $this->logError("An unknown error occurred"); - - return false; - } - - if ($file['size'] > $maxsize) - { - $this->logError("File must be no larger than " . $maxsize . " bytes"); - return false; - } - - if (is_array($allowed_mime) && array_search($file['type'], $allowed_mime) === false) - { - $this->logError("File type is not supported"); - return false; - } - - if (!move_uploaded_file($file['tmp_name'], $path)) - { - $this->logError("Error saving uploaded file"); - return false; - } - } - - else if ($req) - { - $this->logError("File upload is required"); - return false; - } - - return true; - } -} - -?> diff --git a/app/class/framework.class.php b/app/class/framework.class.php deleted file mode 100644 index a3c36cb..0000000 --- a/app/class/framework.class.php +++ /dev/null @@ -1,144 +0,0 @@ -ar() . $_REQUEST['path']; - } - - /* - * Redirect to the given URL and die - */ - function redirectTo($url) - { - header("Location: " . $url); - exit; - } - - /* - * Get a user object for the currently logged in user. Returns false if session is logged out. - */ - function getCurrentUser() - { - if (isset($_SESSION['userguid'])) - { - $user = new User($_SESSION['userguid']); - - if ($user->type == "user") - return $user; - - $this->setCurrentUser(); - } - - return false; - } - - /* - * Get the IP address the client held when the current session began - */ - function getOriginIP() - { - return $_SESSION['userip']; - } - - /* - * Set the current logged in user - */ - function setCurrentUser($user = null) - { - if ($user != null && isset($user->guid)) - { - $_SESSION['userguid'] = $user->guid; - $_SESSION['userip'] = $_SERVER['REMOTE_ADDR']; - } - - else - { - unset($_SESSION['userguid']); - unset($_SESSION['userip']); - } - } - - /* - * Get or create the app's database connection object (this is a singleton object and dependent on system-level config) - */ - static function getDbConnection() - { - global $_SCROTT; - - if (self::$dbobj != null) - return self::$dbobj; - - switch ($_SCROTT['dbEngine']) - { - case "mysql": - $host = $_SCROTT['dbAddress']; - $username = $_SCROTT['dbUser']; - $password = $_SCROTT['dbPass']; - $dbName = $_SCROTT['dbName']; - self::$dbobj = new Mysql($host, $username, $password, $dbName); - break; - - default: - throw new Exception("Problem with Scrott Configuration. Invalid database engine specified."); - break; - } - - return self::$dbobj; - } -} - -?> diff --git a/app/class/group.class.php b/app/class/group.class.php deleted file mode 100644 index 246276a..0000000 --- a/app/class/group.class.php +++ /dev/null @@ -1,49 +0,0 @@ -loadObj($guid); - } - - /* - * Create a new user group object. - * On success, this object should be initialized as the new group (use only on new - * Group() objects) - */ - function createNewGroup($name, $owner) - { - $this->perms = $this->DEFAULT_OBJECT_PERMISSIONS; - $this->owner = $owner->guid; - $this->name = $name; - $this->type = "group"; - - $this->saveObj(); - } -} - -?> diff --git a/app/class/issue.class.php b/app/class/issue.class.php deleted file mode 100644 index 10b1661..0000000 --- a/app/class/issue.class.php +++ /dev/null @@ -1,44 +0,0 @@ -loadObj($guid); - } -} - -?> diff --git a/app/class/message.class.php b/app/class/message.class.php deleted file mode 100644 index ac8444c..0000000 --- a/app/class/message.class.php +++ /dev/null @@ -1,40 +0,0 @@ -loadObj($guid); - } -} - -?> diff --git a/app/class/model.class.php b/app/class/model.class.php deleted file mode 100644 index 7d74b36..0000000 --- a/app/class/model.class.php +++ /dev/null @@ -1,91 +0,0 @@ -errorlist = array(); - $this->warninglist = array(); - $this->noticelist = array(); - } - - /* - * Check for error - */ - function isError() - { - return count($this->errorlist) > 0; - } - - /* - * Check for warning - */ - function isWarning() - { - return count($this->warninglist) > 0; - } - - /* - * Check for notice - */ - function isNotice() - { - return count($this->noticelist) > 0; - } - - /* - * Log an error - */ - function logError($str) - { - $this->errorlist[] = $str; - } - - /* - * Log a warning - */ - function logWarning($str) - { - $this->warninglist[] = $str; - } - - /* - * Log a notice - */ - function logNotice($str) - { - $this->noticelist[] = $str; - } - - /* - * Log errors from a Form - */ - function logFormErrors($obj) - { - $this->errorlist = array_merge($this->errorlist, $obj->errorlist); - } -} - -?> diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php deleted file mode 100644 index f8f456a..0000000 --- a/app/class/mysql.class.php +++ /dev/null @@ -1,77 +0,0 @@ -db = new mysqli($host, $username, $password, $dbName); - - if ($this->db->connect_error) - throw new Exception("Can not connect to Mysql database. Please check your Scrott configuration."); - } - - /* - * Destructor - */ - function __destruct() - { - $this->close(); - } - - /* - * Close connection to DB - */ - function close() - { - $this->db->close(); - } - - /* - * Make a query of the database. Return data as an array of arrays - */ - function query($query) - { - $arr = array(); - $res = $this->db->query($query); - - if ($res === true || $res === false) - return $arr; - - while ($r = $res->fetch_assoc()) - $arr[] = $r; - - return $arr; - } - - /* - * Escape a string for use in a query - */ - function esc($string) - { - return $this->db->real_escape_string($string); - } -} - -?> diff --git a/app/class/object.class.php b/app/class/object.class.php deleted file mode 100644 index 7c0b7bb..0000000 --- a/app/class/object.class.php +++ /dev/null @@ -1,640 +0,0 @@ -db = $this->getDbConnection(); - - $this->table = "object"; - $this->cols = array( - "guid", - "perms", - "owner", - "parent", - "name", - "timeCreated", - "timeUpdated", - "type" - ); - - $this->childTable = $this->db->esc($childTable); - $this->childCols = array(); - - if (is_array($childCols)) - { - foreach ($childCols as $col) - $this->childCols[] = $this->db->esc($col); - } - } - - /* - * Populate this object with data from the DB with a given GUID - */ - function loadObj($guid = null) - { - if (is_null($guid)) - return; - - if (!$this->isGUID($guid)) - return; - - $escdGuid = $this->db->esc($guid); - - /* Base fields */ - $query = "SELECT * FROM `" . $this->table . "` WHERE `guid` = '" . $escdGuid . "'"; - $result = $this->db->query($query)[0]; - - foreach ($this->cols as $col) - { - if (isset($result[$col])) - $this->$col = $result[$col]; - } - - /* Child Table fields */ - $query = "SELECT * FROM `" . $this->childTable . "` WHERE `guid` = '" . $escdGuid . "'"; - $result = $this->db->query($query)[0]; - - foreach ($this->childCols as $col) - { - if (isset($result[$col])) - $this->$col = $result[$col]; - } - } - - /* - * Write this object to the database - */ - function saveObj() - { - if (isset($this->guid)) - { - $this->timeUpdated = $this->getCurrentTimestamp(); - - /* Update Base */ - $updateStr = ""; - - foreach ($this->cols as $col) - { - if (!isset($this->$col)) - continue; - - $updateStr .= "`" . $col . "` = '" . $this->db->esc($this->$col) . "', "; - } - - if (strlen($updateStr) > 0) - { - $updateStr = substr($updateStr, 0, -2); // remove ", " from the end - $query = "UPDATE `" . $this->table . "` SET " . $updateStr . " WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - } - - /* Update Child */ - $updateStr = ""; - - foreach ($this->childCols as $col) - { - if (!isset($this->$col)) - continue; - - $updateStr .= "`" . $col . "` = '" . $this->db->esc($this->$col) . "', "; - } - - if (strlen($updateStr) > 0) - { - $updateStr = substr($updateStr, 0, -2); // remove ", " from the end - $query = "UPDATE `" . $this->childTable . "` SET " . $updateStr . " WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - } - } - - else - { - $this->guid = $this->getNewGUID(); - $this->timeCreated = $this->getCurrentTimestamp(); - $this->timeUpdated = $this->timeCreated; - - /* Insert Base */ - $colsStr = ""; - $valsStr = ""; - - foreach ($this->cols as $col) - { - if (!isset($this->$col)) - continue; - - $colsStr .= "`" . $col . "`, "; - $valsStr .= "'" . $this->db->esc($this->$col) . "', "; - } - - if (strlen($colsStr) > 0) - { - $colsStr = substr($colsStr, 0, -2); // remove ", " - $valsStr = substr($valsStr, 0, -2); - $query = "INSERT INTO `" . $this->table . "` (" . $colsStr . ") VALUES (" . $valsStr . ")"; - $this->db->query($query); - } - - /* Insert Child */ - $colsStr = ""; - $valsStr = ""; - - foreach ($this->childCols as $col) - { - if (!isset($this->$col)) - continue; - - $colsStr .= "`" . $col . "`, "; - $valsStr .= "'" . $this->db->esc($this->$col) . "', "; - } - - if (strlen($colsStr) > 0) - { - $colsStr = substr($colsStr, 0, -2); // remove ", " - $valsStr = substr($valsStr, 0, -2); - $query = "INSERT INTO `" . $this->childTable . "` (" . $colsStr . ") VALUES (" . $valsStr . ")"; - $this->db->query($query); - } - } - } - - /* - * Remove this object from the database - */ - function delObj() - { - if (!isset($this->guid)) - return; - - /* Delete Base */ - $query = "DELETE FROM `" . $this->table . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - - /* Delete Child */ - $query = "DELETE FROM `" . $this->childTable . "` WHERE `guid` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - - /* obj_member garbage collection */ - $query = "DELETE FROM `obj_member` WHERE `guid` = '" . $this->db->esc($this->guid) . "' OR `member` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - - /* msg_read garbage collection */ - $query = "DELETE FROM `msg_read` WHERE `guid` = '" . $this->db->esc($this->guid) . "' OR `user` = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); - } - - /* - * Get current timestamp for object database purposes - */ - function getCurrentTimestamp() - { - $query = "SELECT now() AS stamp"; - $result = $this->db->query($query); - return $result[0]['stamp']; - } - - /* - * Check whether given GUID exists - */ - function isGUID($guid) - { - $query = "SELECT `guid` FROM `object` WHERE `guid` = '" . $this->db->esc($guid) . "'"; - $result = $this->db->query($query); - - if (count($result) > 0) - return true; - - return false; - } - - /* - * Get a new, unique GUID for a new system object - */ - function getNewGUID() - { - do - { - $guid = substr($this->getBlob(), 0, 8); - } - while ($this->isGUID($guid)); - - return $guid; - } - - /* - * Get a random sha256 blob - */ - function getBlob() - { - return hash("sha256", openssl_random_pseudo_bytes(64)); - } - - /* - * Get a user object for this object's owner - */ - function getOwner() - { - if (isset($this->owner)) - return new User($this->owner); - - return null; - } - - /* - * Get an array of all members of this object - */ - function getMembers() - { - $query = "SELECT member FROM obj_member WHERE guid = '" . $this->db->esc($this->guid) . "'"; - $result = $this->db->query($query); - - $members = array(); - - foreach ($result as $m) - $members[] = new User($m['member']); - - return $members; - } - - /* - * Check if given user (or group) is the owner of this object - */ - function isOwner($ug) - { - return $this->getOwner()->guid == $ug->guid; - } - - /* - * Check if given user (or group) is a member of this object - */ - function isMember($ug) - { - foreach ($this->getMembers() as $member) - { - if ($member->guid == $ug->guid) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canAccess($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user)) - return true; - - if ($this->perms & 0x004) // accessible by public - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canAccessSub($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canAccessSub($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModify($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x100) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySub($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySub($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModifyMembers($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x080) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySubMembers($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySubMembers($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModifyPermissions($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySubPermissions($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySubPermissions($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canAccessSub($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x040) - return true; - - if ($this->perms & 0x002) // accessible by public - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canAccessSub($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canAccessSub($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canCreateSub($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x020) - return true; - - if ($this->perms & 0x001) // accessible by public - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canCreateSub($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canCreateSub($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModifySub($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x010) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySub($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySub($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModifySubMembers($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->isMember($user) && $this->perms & 0x008) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySubMembers($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySubMembers($user)) - return true; - } - - return false; - } - - /* - * Check if given user has permissions for this object - */ - function canModifySubPermissions($user) - { - if ($user->admin) - return true; - - if ($this->isOwner($user)) - return true; - - if ($this->parent != "") - { - $parent = new DBObject($this->parent); - - if ($parent->canModifySubPermissions($user)) - return true; - } - else if ($this->owner != $this->guid) - { - $owner = new DBObject($this->owner); - - if ($owner->canModifySubPermissions($user)) - return true; - } - - return false; - } - - /* - * Get URL to this object - */ - function getURL() - { - return $this->ar() . "/" . $this->guid; - } - - /* - * Get object's head image - */ - function getHeadImage() - { - return $this->ar() . "/file.php?d=img/heads&f=" . $this->guid; - } - - /* - * Remove this object's head image - */ - function rmHeadImage() - { - if (!is_file("assets/img/heads/" . $this->guid)) - return true; - - return unlink("assets/img/heads/" . $this->guid); - } -} - -/* - * Concrete Database Object which can be used in a polymorphic way - */ -class DBObject extends Object -{ - /* - * Constructor - */ - function __construct($guid = null) - { - parent::__construct(); - $this->loadObj($guid); - } -} - -?> diff --git a/app/class/pad.class.php b/app/class/pad.class.php deleted file mode 100644 index 32994e5..0000000 --- a/app/class/pad.class.php +++ /dev/null @@ -1,40 +0,0 @@ -loadObj($guid); - } -} - -?> diff --git a/app/class/setting.class.php b/app/class/setting.class.php deleted file mode 100644 index c0965a3..0000000 --- a/app/class/setting.class.php +++ /dev/null @@ -1,90 +0,0 @@ -esc($key); - - $query = "SELECT `value` FROM `setting` WHERE `key` = '" . $escdKey . "'"; - $res = $db->query($query); - - if (count($res) == 0) - return false; - - return $res[0]['value']; - } - - /* - * Helper function for setting setting values on the database - */ - static function setValue($key, $value) - { - $db = parent::getDbConnection(); - $escdKey = $db->esc($key); - $escdValue = $db->esc($value); - - if (self::getValue($key) === false) - $query = "INSERT INTO setting (`key`, value) VALUES('" . $escdKey . "', '" . $escdValue . "')"; - else - $query = "UPDATE setting SET value = '" . $escdValue . "' WHERE `key` = '" . $escdKey . "'"; - - $db->query($query); - } - - /* - * Force or forbid SSL connections? - */ - static function settSSL($value = null) - { - $opt = "settSSL"; - - if ($value != null) - self::setValue($opt, $value); - - $value = self::getValue($opt); - - if ($value === false) - return "neither"; - - return $value; - } - - /* - * Should the app allow the public to signup their own accounts with Scrott? - */ - static function allowPublicSignup($value = null) - { - $opt = "allowPublicSignup"; - - if ($value != null) - self::setValue($opt, $value); - - return self::getValue($opt); - } -} - -?> diff --git a/app/class/stage.class.php b/app/class/stage.class.php deleted file mode 100644 index 1a2aadb..0000000 --- a/app/class/stage.class.php +++ /dev/null @@ -1,39 +0,0 @@ -loadObj($guid); - } -} - -?> diff --git a/app/class/user.class.php b/app/class/user.class.php deleted file mode 100644 index b8143a9..0000000 --- a/app/class/user.class.php +++ /dev/null @@ -1,241 +0,0 @@ -loadObj($guid); - } - - /* - * Initialize object by username - */ - function initByUsername($username) - { - $query = "SELECT guid FROM object WHERE type = 'user' AND name = '" . $this->db->esc($username) . "'"; - $result = $this->db->query($query); - - if (count($result) == 0) - return false; - - $this->loadObj($result[0]['guid']); - return true; - } - - /* - * Get all users -- ordered by name, ascending - */ - function getAllUsers_orderByName() - { - $query = "SELECT guid FROM `object` WHERE `type` = 'user' ORDER BY name"; - $result = $this->db->query($query); - - $users = array(); - - foreach ($result as $u) - $users[] = new User($u['guid']); - - return $users; - } - - /* - * Get all users -- ordered by admin DESC (admins first), then by name - */ - function getAllUsers_orderByAdminByName() - { - $query = "SELECT o.guid FROM object o JOIN user u ON o.guid = u.guid WHERE o.type = 'user' ORDER BY u.admin DESC, o.name"; - $result = $this->db->query($query); - - $users = array(); - - foreach ($result as $u) - $users[] = new User($u['guid']); - - return $users; - } - - /* - * Get the number of administrative accounts in the system - */ - function getNumAdmins() - { - $query = "SELECT count(*) as cnt FROM user WHERE admin = 1"; - $results = $this->db->query($query); - return $results[0]['cnt']; - } - - /* - * Check whether a given username is currently in use - */ - function usernameInUse($username) - { - $escd_username = $this->db->esc($username); - - $query = "SELECT name FROM object WHERE type = 'user' AND name = '" . $escd_username . "'"; - $results = $this->db->query($query); - - if (count($results) > 0) - return true; - - return false; - } - - /* - * Generate a key from a user's password and salt - */ - function getKey($password, $salt) - { - return hash("sha256", $salt . $password); - } - - /* - * Create a new User object with the given username and keyed with the given plain-text password - * This function returns false if $username is already being used - * On success, this object should be initialized as the new user (use only on new User() objects) - */ - function createNewUser($username, $password) - { - if ($this->usernameInUse($username)) - return false; - - /* if there exist no users already, make this new one an admin */ - if (count($this->getAllUsers_orderByName()) == 0) - $this->admin = 1; - - $this->perms = 0; - $this->name = $username; - $this->type = "user"; - $this->setPassword($password); - $this->setEmail(""); - - $this->saveObj(); - - $this->owner = $this->guid; - $this->saveObj(); - - return true; - } - - /* - * Validate the password for this user. Returns true if correct, false otherwise - */ - function validatePassword($password) - { - $key = $this->getKey($password, $this->salt); - return $key == $this->key; - } - - /* - * Validate the email confirmation key for a user, returns true if correct, false otherwise. On success, $this->emailConf is also set to 1 - */ - function confirmEmailKey($key) - { - if ($key != $this->emailConfKey) - return false; - - $this->emailConf = 1; - return true; - } - - /* - * Overwrite the salt and key for this user, given a new plaintext password - */ - function setPassword($password) - { - $this->salt = $this->getBlob(); - $this->key = $this->getKey($password, $this->salt); - } - - /* - * Overwrite the emailConfKey and flag, and change user's saved email address - */ - function setEmail($email) - { - $this->email = $email; - $this->emailConf = 0; - $this->emailConfKey = $this->getBlob(); - } - - /* - * If a user has an alias set, display it instead of their username - */ - function getDisplayName() - { - if ($this->alias != "") - return $this->alias; - - return $this->name; - } - - /* - * Get the glyphicon to use for this user - */ - function getGlyphicon() - { - if ($this->admin) - return "glyphicon glyphicon-sunglasses"; - - return "glyphicon glyphicon-user"; - } - - /* - * Get all groups this user owns or is a member of - */ - function getGroups() - { - /* owner */ - $query = "SELECT guid FROM object WHERE type = 'group' AND owner = '" . $this->db->esc($this->guid) . "'"; - $result = $this->db->query($query); - - $groups = array(); - - foreach ($result as $g) - $groups[] = new Group($g['guid']); - - /* member */ - $query = "SELECT o.guid FROM object o JOIN obj_member om ON o.guid = om.guid WHERE o.type = 'group' AND member = '" . $this->db->esc($this->guid) . "'"; - $result = $this->db->query($query); - - foreach ($result as $g) - $groups[] = new Group($g['guid']); - - return $groups; - } -} - -?> -- cgit v1.2.3 From 2e4dbf98b96adc8731c3101385e47e1f00c21d31 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 23 Oct 2016 17:21:16 -0400 Subject: Add database class --- app/class/database.class.php | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 app/class/database.class.php (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php new file mode 100644 index 0000000..c791088 --- /dev/null +++ b/app/class/database.class.php @@ -0,0 +1,46 @@ + -- cgit v1.2.3 From b6f82bb6552517d8bc442a2087c6c37a33bd18bd Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 23 Oct 2016 19:01:52 -0400 Subject: Add mysql class --- app/class/database.class.php | 1 + app/class/mysql.class.php | 74 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 app/class/mysql.class.php (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index c791088..6c6ecd6 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -23,6 +23,7 @@ abstract class Database { private static $instance = NULL; + protected $db; /* * Return the database instance object, creating it if this is the diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php new file mode 100644 index 0000000..90a4016 --- /dev/null +++ b/app/class/mysql.class.php @@ -0,0 +1,74 @@ +db = new mysqli($host, $username, $password, $dbName); + + if ($this->db->connect_error) + throw new Exception("Can not connect to MySQL database. Please check your configuration."); + } + + /* + * Destructor + */ + public function __destruct() + { + $this->close(); + } + + /* + * Close connection to DB + */ + public function close() + { + $this->db->close(); + } + + /* + * Make a query of the database. Return data as an array of arrays. + */ + public function query(string $query) : array + { + $arr = array(); + $res = $this->db->query($query); + + if ($res === true || $res === false) + return $arr; + + while (($arr[] = $res->fetch_assoc())); + return $arr; + } + + /* + * Escape a string for use in a query + */ + public function esc(string $str) : string + { + return $this->db->real_escape_string($str); + } +} + +?> -- cgit v1.2.3 From 33499cb813d6aac2abc649dd8e42a3c97ce306b2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 14 Jan 2017 02:13:07 -0500 Subject: Partial commit of initObj function -- this class needs reworked again... --- app/class/database.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index 6c6ecd6..c7ef65b 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -42,6 +42,32 @@ abstract class Database public abstract function close(); public abstract function query(string $query) : array; public abstract function esc(string $str) : string; + + /* + * This function will lookup the row from the database on the given + * table containing the given GUID and initialize the class properties + * on this object based on the given field list. + */ + public function initObj(string $table, array $fields, string $guid = NULL) + { + if (is_null($guid)) + return; + + $guid = $this->esc($guid); + $query = "SELECT * FROM " . $table . " WHERE guid = '" . $guid . "'"; + $res = $this->query($query); + + if (!count($res)) + return; + + $res = $res[0]; + + foreach ($fields as $field) + { + if (isset($res[$field])) + $this->$field = $res[$field]; + } + } } ?> -- cgit v1.2.3 From dae3964e7682dcd0d64075dfc28a23c12ef6c52e Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 14 Jan 2017 02:26:28 -0500 Subject: Reset working directory for clean Scrott implementation --- app/class/database.class.php | 73 ------------------------------------------- app/class/mysql.class.php | 74 -------------------------------------------- 2 files changed, 147 deletions(-) delete mode 100644 app/class/database.class.php delete mode 100644 app/class/mysql.class.php (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php deleted file mode 100644 index c7ef65b..0000000 --- a/app/class/database.class.php +++ /dev/null @@ -1,73 +0,0 @@ -esc($guid); - $query = "SELECT * FROM " . $table . " WHERE guid = '" . $guid . "'"; - $res = $this->query($query); - - if (!count($res)) - return; - - $res = $res[0]; - - foreach ($fields as $field) - { - if (isset($res[$field])) - $this->$field = $res[$field]; - } - } -} - -?> diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php deleted file mode 100644 index 90a4016..0000000 --- a/app/class/mysql.class.php +++ /dev/null @@ -1,74 +0,0 @@ -db = new mysqli($host, $username, $password, $dbName); - - if ($this->db->connect_error) - throw new Exception("Can not connect to MySQL database. Please check your configuration."); - } - - /* - * Destructor - */ - public function __destruct() - { - $this->close(); - } - - /* - * Close connection to DB - */ - public function close() - { - $this->db->close(); - } - - /* - * Make a query of the database. Return data as an array of arrays. - */ - public function query(string $query) : array - { - $arr = array(); - $res = $this->db->query($query); - - if ($res === true || $res === false) - return $arr; - - while (($arr[] = $res->fetch_assoc())); - return $arr; - } - - /* - * Escape a string for use in a query - */ - public function esc(string $str) : string - { - return $this->db->real_escape_string($str); - } -} - -?> -- cgit v1.2.3 From a92f8af0f9aed383e4243e5b2b50d248e843cab4 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 14 Jan 2017 21:11:31 -0500 Subject: Add database class --- app/class/database.class.php | 58 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/class/database.class.php (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php new file mode 100644 index 0000000..4f26c45 --- /dev/null +++ b/app/class/database.class.php @@ -0,0 +1,58 @@ +close(); + } + + /* + * Return the database instance object, creating it if this is the + * first call to this function. This function will need maintained + * as new DBMSs are supported. + */ + public static function getInstance() : database + { + // TODO + } + + /* + * These functions are to be implemented by DBMS extensions, + * providing a uniform interface to database engines. + */ + public abstract function close() : void; + public abstract function query(string $query) : array; + public abstract function esc(string $str) : string; +} + +?> -- cgit v1.2.3 From a17463ee221da81d394d4571e3e9b0f2e52d965f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 15 Jan 2017 04:43:33 -0500 Subject: Add table class --- app/class/table.class.php | 173 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/class/table.class.php (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php new file mode 100644 index 0000000..62f95d3 --- /dev/null +++ b/app/class/table.class.php @@ -0,0 +1,173 @@ +val to + * this array that represents the table being modeled. This array is + * used by db table IO functions to construct SQL queries. + */ + protected $fields = array(); + + protected $db; + + /* + * Instanciate an object representing an existing database entity + * named by the given GUID. If no such GUID exists, a + * databasekeyexception is thrown. If NULL is passed, no database + * lookups are attempted. This is helpful for assembling new database + * objects. + */ + public function __construct(?string $guid = NULL) + { + $this->db = database::getInstance(); + + if ($guid) + $this->loadObj($guid); + } + + /* + * This function will iterate the $fields array and initialize this + * object's class properties based on the table=>fields mapping. If + * no such GUID exists, a databasekeyexception is thrown. + */ + private function loadObj(string $guid) : void + { + $guid = $this->db->esc($guid); + + if (!$this->isGUID($guid)) + throw new databasekeyexception("GUID " . $guid . " does not exist"); + + foreach ($this->fields as $tbl => $flds) + { + $tbl = $this->db->esc($tbl); + $query = "SELECT * FROM " . $tbl . " WHERE guid = '" . $guid . "'"; + $res = $this->db->query($query)[0]; + + foreach ($flds as $fld) + { + if (isset($res[$fld])) + $this->$fld = $res[$fld]; + } + } + } + + /* + * This function will update this object in the database, or insert new + * data if this object does not yet have a GUID. This function uses the + * $fields array to construct SQL queries. + */ + public function saveObj() : void + { + /* update existing object */ + if (isset($this->guid)) + { + $this->updated = $this->getCurrentTimestamp(); + + foreach ($this->fields as $tbl => $flds) + { + $tbl = $this->db->esc($tbl); + $udstr = ""; + + foreach ($flds as $fld) + { + if (!isset($this->$fld)) + continue; + + $fld = $this->db->esc($fld); + $udstr .= $fld . " = '" . $this->db->esc($this->$fld) . "', "; + } + + if (strlen($udstr) > 0) + { + $udstr = substr($udstr, 0, -2); // remove ", " from the end + $query = "UPDATE " . $tbl . " SET " . $udstr . " WHERE guid = '" . $this->db->esc($this->guid) . "'"; + $this->db->query($query); + } + } + } + + /* create new object */ + else + { + $this->guid = $this->getNewGUID(); + $this->created = $this->getCurrentTimestamp(); + $this->updated = $this->created; + + foreach ($this->fields as $tbl => $flds) + { + $tbl = $this->db->esc($tbl); + $fldstr = ""; + $valstr = ""; + + foreach ($flds as $fld) + { + if (!isset($this->$fld)) + continue; + + $fld = $this->db->esc($fld); + $fldstr .= $fld . ", "; + $valstr .= "'" . $this->db->esc($this->$fld) . "', "; + } + + if (strlen($fldstr) > 0) + { + $fldstr = substr($fldstr, 0, -2); // remove ", " + $valstr = substr($valstr, 0, -2); + $query = "INSERT INTO " . $tbl . " (" . $fldstr . ") VALUES (" . $valstr . ")"; + $this->db->query($query); + } + } + } + } + + /* + * This function will remove this object from the database by deleting + * rows with this object's GUID from tables in $fields. If this object + * does not have a GUID, throw a databasekeyexception. + */ + public function delObj() : void + { + if (!isset($this->guid)) + throw new databasekeyexception("GUID (null) does not exist"); + + $guid = $this->db->esc($this->guid); + + foreach ($this->fields as $tbl => $flds) + { + $tbl = $this->db->esc($tbl); + $query = "DELETE FROM " . $tbl . " WHERE guid = '" . $guid . "'"; + $this->db->query($query); + } + + /* garbage collection */ + $query = "DELETE FROM members WHERE guid = '" . $guid . "' OR member = '" . $guid . "'"; + $this->db->query($query); + + $query = "DELETE FROM views WHERE guid = '" . $guid . "' OR viewer = '" . $guid . "'"; + $this->db->query($query); + } +} + +?> -- cgit v1.2.3 From 92491a36a3f5f02d11b2aa9bf8f3c1418af6e558 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 15 Jan 2017 20:29:14 -0500 Subject: Add mysql class --- app/class/mysql.class.php | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/class/mysql.class.php (limited to 'app/class') diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php new file mode 100644 index 0000000..fae09bb --- /dev/null +++ b/app/class/mysql.class.php @@ -0,0 +1,68 @@ +db = new mysqli($host, $uname, $passwd, $dbname); + + if ($this->db->connect_error) + throw new Exception("Can not connect to MySQL database. Please check your configuration."); + } + + /* + * Close connection to DB + */ + public function close() : void + { + $this->db->close(); + } + + /* + * Make a query of the database. Return data as an array of arrays. + */ + public function query(string $query) : array + { + $arr = array(); + $res = $this->db->query($query); + + if ($res === true || $res === false) + return $arr; + + while (($arr[] = $res->fetch_assoc())); + return $arr; + } + + /* + * Escape a string for use in a query + */ + public function esc(string $str) : string + { + return $this->db->real_escape_string($str); + } +} + +?> -- cgit v1.2.3 From ea0b5e57956985b360afae948553aaad5cbb75d7 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 16 Jan 2017 04:01:49 -0500 Subject: Add object class --- app/class/object.class.php | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 app/class/object.class.php (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php new file mode 100644 index 0000000..a000a10 --- /dev/null +++ b/app/class/object.class.php @@ -0,0 +1,89 @@ +fields['objects'] = array( + "guid", + "owner", + "parent", + "name", + "created", + "updated", + + "membModify", + "membMemb", + "membAccs", + "membCres", + "membModifys", + "membMembs", + "pubAcc", + "pubAccs", + "pubCres", + + "objtype", + ); + + parent::__construct($guid); + } + + /* + * Get the owner of this object. Either a user object or a group + * object will be returned. If this object does not have an owner, + * NULL will be returned. + */ + public function getOwner() : agent + { + if (!isset($this->owner) || $this->owner == "") + return NULL; + + $obj = new object($this->owner); + + if ($obj->objtype == "group") + return new group($this->owner); + + return new user($this->owner); + } + + /* + * Get an array of all members of this object + */ + public function getMembers() : array + { + $memb = array(); + $query = "SELECT member FROM members WHERE guid = '" . $this->db->esc($this->guid) . "'"; + $res = $this->db->query($query); + + foreach ($res as $m) + $memb[] = new user($m['member']); + + return $memb; + } +} + +?> -- cgit v1.2.3 From 66cf5c4d36ed6020c6ee6b7ca99aecd5a8f3bcf4 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 17 Jan 2017 03:56:28 -0500 Subject: Add helper functions to table class --- app/class/table.class.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 62f95d3..0edff9d 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -168,6 +168,45 @@ abstract class table $query = "DELETE FROM views WHERE guid = '" . $guid . "' OR viewer = '" . $guid . "'"; $this->db->query($query); } + + /* + * Get a random sha256 blob, returned as a hexadecimal string + */ + public function getBlob() : string + { + return hash("sha256", openssl_random_pseudo_bytes(64)); + } + + /* + * Get current timestamp as a string for object database purposes + */ + private function getCurrentTimestamp() : string + { + $query = "SELECT now() AS stamp"; + $res = $this->db->query($query); + return $res[0]['stamp']; + } + + /* + * Check whether the given GUID exists + */ + private function isGUID(string $guid) : bool + { + $guid = $this->db->esc($guid); + $query = "SELECT guid FROM objects WHERE guid = '" . $guid . "'"; + $res = $this->db->query($query); + return count($res) > 0; + } + + /* + * Get a new, unique GUID for a new system object + */ + private function getNewGUID() : string + { + do $guid = substr($this->getBlob(), 0, 8); + while ($this->isGUID($guid)); + return $guid; + } } ?> -- cgit v1.2.3 From 7342f00a0624ec0e89732ad476f44dc95d0129de Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 4 Feb 2017 18:45:06 -0500 Subject: Fix to object class function signature The getOwner() function should have had a nullable return type. --- app/class/object.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index a000a10..8ad17f5 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -57,7 +57,7 @@ class object extends table * object will be returned. If this object does not have an owner, * NULL will be returned. */ - public function getOwner() : agent + public function getOwner() : ?agent { if (!isset($this->owner) || $this->owner == "") return NULL; -- cgit v1.2.3 From 19fac440ae8cf1a03e491825fb7d33313c451caa Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 4 Feb 2017 22:10:43 -0500 Subject: Add agent class --- app/class/agent.class.php | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/class/agent.class.php (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php new file mode 100644 index 0000000..7c3b23c --- /dev/null +++ b/app/class/agent.class.php @@ -0,0 +1,54 @@ +getOwner()->guid == $this->guid; + } + + /* + * Check whether this agent is a member of the given object + */ + public function isMemberOf(object $obj) : bool + { + foreach ($obj->getMembers() as $memb) + { + if ($memb->guid == $this->guid) + return true; + } + + return false; + } +} + +?> -- cgit v1.2.3 From 9642aeb3b2fa76fd21a4fa0878bc226f6ff013a5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 5 Feb 2017 00:42:29 -0500 Subject: Remove custom exception 'databasekeyexception' Just use a generic exception in these cases. I don't want to handle these any differently, and just fall back on the main Exception() error page once we get to a UI. --- app/class/table.class.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 0edff9d..be7a375 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -13,7 +13,6 @@ */ require_once "class/database.class.php"; -require_once "class/databasekeyexception.class.php"; /* * This class provides functionality for retrieving data from and updating @@ -34,10 +33,9 @@ abstract class table /* * Instanciate an object representing an existing database entity - * named by the given GUID. If no such GUID exists, a - * databasekeyexception is thrown. If NULL is passed, no database - * lookups are attempted. This is helpful for assembling new database - * objects. + * named by the given GUID. If no such GUID exists, an exception + * is thrown. If NULL is passed, no database lookups are attempted. + * This is helpful for assembling new database objects. */ public function __construct(?string $guid = NULL) { @@ -50,14 +48,14 @@ abstract class table /* * This function will iterate the $fields array and initialize this * object's class properties based on the table=>fields mapping. If - * no such GUID exists, a databasekeyexception is thrown. + * no such GUID exists, an exception is thrown. */ private function loadObj(string $guid) : void { $guid = $this->db->esc($guid); if (!$this->isGUID($guid)) - throw new databasekeyexception("GUID " . $guid . " does not exist"); + throw new Exception("GUID " . $guid . " does not exist"); foreach ($this->fields as $tbl => $flds) { @@ -145,12 +143,12 @@ abstract class table /* * This function will remove this object from the database by deleting * rows with this object's GUID from tables in $fields. If this object - * does not have a GUID, throw a databasekeyexception. + * does not have a GUID, throw an exception. */ public function delObj() : void { if (!isset($this->guid)) - throw new databasekeyexception("GUID (null) does not exist"); + throw new Exception("GUID (null) does not exist"); $guid = $this->db->esc($this->guid); -- cgit v1.2.3 From 27cfca44b3c43c8bbd3ea5ac602e1f34753e1e90 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 5 Feb 2017 02:33:55 -0500 Subject: Fix bug in mysql query function Need to remove the NULL return value from fetch_assoc() to fix usages of count($res) for determining the number of results from a MySQL query. This NULL return value is returned from mysqli's fetch_assoc() function to signal no more result rows. --- app/class/mysql.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php index fae09bb..db0eb7d 100644 --- a/app/class/mysql.class.php +++ b/app/class/mysql.class.php @@ -53,7 +53,7 @@ class mysql extends database return $arr; while (($arr[] = $res->fetch_assoc())); - return $arr; + return array_filter($arr, function ($val) { return !is_null($val); }); } /* -- cgit v1.2.3 From be9dd0f28fb63e46dfbafc8f9cc1764ac89cad92 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 5 Feb 2017 17:57:49 -0500 Subject: Add user class --- app/class/user.class.php | 183 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 app/class/user.class.php (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php new file mode 100644 index 0000000..8d7da45 --- /dev/null +++ b/app/class/user.class.php @@ -0,0 +1,183 @@ +fields['users'] = array( + "guid", + "auth", + "salt", + "alias", + "email", + "emailVer", + "admin", + "reg", + "emailConf", + ); + + parent::__construct($guid); + } + + /* + * Get the GUID of a user object from a given username, or NULL if + * the username is not in use. Therefore, this function can be + * used to test the existence of a user with the given username. + */ + public static function getGuidByUname(string $uname) : ?string + { + $uname = $this->db->esc($uname); + + $query = "SELECT guid FROM objects WHERE objtype = 'user' AND name = '" . $uname . "'"; + $res = $this->db->query($query); + + if (count($res) == 0) + return NULL; + + return $res[0]['guid']; + } + + /* + * Get a user object from a given username, or NULL if the username + * is not in use. This function can be used to test the existence + * of a user with the given username. + */ + public static function getByUname(string $uname) : ?user + { + if (($guid = self::getGuidByUname($uname))) + return new user($guid); + + return NULL; + } + + /* + * Get an array of all users, sorted by username + */ + public static function getAll_ordByUname() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'user' ORDER BY name"; + $res = $this->db->query($query); + + $users = array(); + + foreach ($res as $u) + $users[] = new user($u['guid']); + + return $users; + } + + /* + * Get an array of all users, sorted by admin (descending, admins + * first), then by username. + */ + public static function getAll_ordByAdminByUname() : array + { + $query = "SELECT o.guid FROM objects o JOIN users u ON o.guid = u.guid " . + "WHERE o.objtype = 'user' ORDER BY u.admin DESC, o.name"; + $res = $this->db->query($query); + + $users = array(); + + foreach ($res as $u) + $users[] = new user($u['guid']); + + return $users; + } + + /* + * Get an array of all admins, sorted by username + */ + public static function getAllAdmin_ordByUname() : array + { + $query = "SELECT o.guid FROM objects o JOIN users u ON o.guid = u.guid " . + "WHERE o.objtype = 'user' AND u.admin = 1 ORDER BY o.name"; + $res = $this->db->query($query); + + $users = array(); + + foreach ($res as $u) + $users[] = new user($u['guid']); + + return $users; + } + + /* + * Get the currently logged in user, or NULL if logged out. This + * function will throw if unable to aquire a PHP session. This + * function will also forcibly log the current user out if it + * detects any changes in the user-agent or remote IP address. + */ + public static function getCurrent() : ?user + { + if (!session_start()) + throw new Exception("Unable to aquire a PHP session"); + + if (!isset($_SESSION['userguid'])) + return NULL; + + /* detect session hijacking */ + if (($_SESSION['useragent'] != $_SERVER['HTTP_USER_AGENT']) || + ($_SESSION['userip'] != $_SERVER['REMOTE_ADDR'])) + { + self::setCurrent(); + return NULL; + } + + return new user($_SESSION['userguid']); + } + + /* + * Set the currently logged in user. Using NULL will logout any + * current user. This function will throw if unable to aquire a + * PHP session. This function will also cache the user-agent and + * remote IP address of the current request to help validate future + * requests made under the same session. + */ + public static function setCurrent(?user $user = NULL) : void + { + if (!session_start()) + throw new Exception("Unable to aquire a PHP session"); + + unset($_SESSION['userguid']); + unset($_SESSION['useragent']); + unset($_SESSION['userip']); + + if ($user) + { + $_SESSION['userguid'] = $user->guid; + $_SESSION['useragent'] = $_SERVER['HTTP_USER_AGENT']; + $_SESSION['userip'] = $_SERVER['REMOTE_ADDR']; + } + } + + /* + * Get the salted and hashed form of a password + */ + private static function getAuth(string $passwd, string $salt) : string + { + return hash("sha256", $passwd . $salt); + } +} + +?> -- cgit v1.2.3 From 0746eb18ff79bb94e2b1f1dca866b5541d4340cb Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 5 Feb 2017 18:02:00 -0500 Subject: Add global source code file --- app/class/globals.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/class/globals.php (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php new file mode 100644 index 0000000..3512961 --- /dev/null +++ b/app/class/globals.php @@ -0,0 +1,22 @@ + -- cgit v1.2.3 From ce38fd96e1105c70b55196ae3b6ab612442c8b2f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 5 Feb 2017 21:46:36 -0500 Subject: Add redirect to forceful logout While forcing a logout, we need to also redirect to the app root. --- app/class/user.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 8d7da45..fc969fa 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -141,6 +141,7 @@ class user extends agent ($_SESSION['userip'] != $_SERVER['REMOTE_ADDR'])) { self::setCurrent(); + location("/"); return NULL; } -- cgit v1.2.3 From 56a6dda13bb85b25f590fc8a64535bb53c3c2fd2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 6 Feb 2017 00:47:55 -0500 Subject: Update database API The abstract functions of database have been made protected and their names prefixed with '_'. The database class has been given new static functions query() and esc(), which call the _query() and _esc() function from the database instance object. This change was made to address the use of db routines from static contexes. Calls like `database::get()->query()` which mix static and instance function access operators, can now be `database::query()`, and all singleton is abstracted away; the instance's destructor continues to close the db connection. --- app/class/database.class.php | 27 ++++++++++++++++++++++----- app/class/mysql.class.php | 6 +++--- 2 files changed, 25 insertions(+), 8 deletions(-) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index 4f26c45..c0f13d7 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -33,7 +33,7 @@ abstract class database */ public function __destruct() { - $this->close(); + $this->_close(); } /* @@ -41,7 +41,7 @@ abstract class database * first call to this function. This function will need maintained * as new DBMSs are supported. */ - public static function getInstance() : database + private static function getInstance() : database { // TODO } @@ -50,9 +50,26 @@ abstract class database * These functions are to be implemented by DBMS extensions, * providing a uniform interface to database engines. */ - public abstract function close() : void; - public abstract function query(string $query) : array; - public abstract function esc(string $str) : string; + protected abstract function _close() : void; + protected abstract function _query(string $query) : array; + protected abstract function _esc(string $str) : string; + + /* + * Perform a database query and return the results as an array + * of arrays. + */ + public static function query(string $query) : array + { + return self::getInstance()->_query($query); + } + + /* + * Escape a given string for use in a database query + */ + public static function esc(string $str) : string + { + return self::getInstance()->_esc($str); + } } ?> diff --git a/app/class/mysql.class.php b/app/class/mysql.class.php index db0eb7d..57a9819 100644 --- a/app/class/mysql.class.php +++ b/app/class/mysql.class.php @@ -36,7 +36,7 @@ class mysql extends database /* * Close connection to DB */ - public function close() : void + protected function _close() : void { $this->db->close(); } @@ -44,7 +44,7 @@ class mysql extends database /* * Make a query of the database. Return data as an array of arrays. */ - public function query(string $query) : array + protected function _query(string $query) : array { $arr = array(); $res = $this->db->query($query); @@ -59,7 +59,7 @@ class mysql extends database /* * Escape a string for use in a query */ - public function esc(string $str) : string + protected function _esc(string $str) : string { return $this->db->real_escape_string($str); } -- cgit v1.2.3 From 476192ca8fa2053af74a7e7f5e4006c83c8d0cad Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 6 Feb 2017 01:18:10 -0500 Subject: Update table class tree to use static database references --- app/class/object.class.php | 4 ++-- app/class/table.class.php | 44 ++++++++++++++++++++------------------------ app/class/user.class.php | 10 +++++----- 3 files changed, 27 insertions(+), 31 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 8ad17f5..6a77b37 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -76,8 +76,8 @@ class object extends table public function getMembers() : array { $memb = array(); - $query = "SELECT member FROM members WHERE guid = '" . $this->db->esc($this->guid) . "'"; - $res = $this->db->query($query); + $query = "SELECT member FROM members WHERE guid = '" . database::esc($this->guid) . "'"; + $res = database::query($query); foreach ($res as $m) $memb[] = new user($m['member']); diff --git a/app/class/table.class.php b/app/class/table.class.php index be7a375..0b9a53c 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -29,8 +29,6 @@ abstract class table */ protected $fields = array(); - protected $db; - /* * Instanciate an object representing an existing database entity * named by the given GUID. If no such GUID exists, an exception @@ -39,8 +37,6 @@ abstract class table */ public function __construct(?string $guid = NULL) { - $this->db = database::getInstance(); - if ($guid) $this->loadObj($guid); } @@ -52,16 +48,16 @@ abstract class table */ private function loadObj(string $guid) : void { - $guid = $this->db->esc($guid); + $guid = database::esc($guid); if (!$this->isGUID($guid)) throw new Exception("GUID " . $guid . " does not exist"); foreach ($this->fields as $tbl => $flds) { - $tbl = $this->db->esc($tbl); + $tbl = database::esc($tbl); $query = "SELECT * FROM " . $tbl . " WHERE guid = '" . $guid . "'"; - $res = $this->db->query($query)[0]; + $res = database::query($query)[0]; foreach ($flds as $fld) { @@ -85,7 +81,7 @@ abstract class table foreach ($this->fields as $tbl => $flds) { - $tbl = $this->db->esc($tbl); + $tbl = database::esc($tbl); $udstr = ""; foreach ($flds as $fld) @@ -93,15 +89,15 @@ abstract class table if (!isset($this->$fld)) continue; - $fld = $this->db->esc($fld); - $udstr .= $fld . " = '" . $this->db->esc($this->$fld) . "', "; + $fld = database::esc($fld); + $udstr .= $fld . " = '" . database::esc($this->$fld) . "', "; } if (strlen($udstr) > 0) { $udstr = substr($udstr, 0, -2); // remove ", " from the end - $query = "UPDATE " . $tbl . " SET " . $udstr . " WHERE guid = '" . $this->db->esc($this->guid) . "'"; - $this->db->query($query); + $query = "UPDATE " . $tbl . " SET " . $udstr . " WHERE guid = '" . database::esc($this->guid) . "'"; + database::query($query); } } } @@ -115,7 +111,7 @@ abstract class table foreach ($this->fields as $tbl => $flds) { - $tbl = $this->db->esc($tbl); + $tbl = database::esc($tbl); $fldstr = ""; $valstr = ""; @@ -124,9 +120,9 @@ abstract class table if (!isset($this->$fld)) continue; - $fld = $this->db->esc($fld); + $fld = database::esc($fld); $fldstr .= $fld . ", "; - $valstr .= "'" . $this->db->esc($this->$fld) . "', "; + $valstr .= "'" . database::esc($this->$fld) . "', "; } if (strlen($fldstr) > 0) @@ -134,7 +130,7 @@ abstract class table $fldstr = substr($fldstr, 0, -2); // remove ", " $valstr = substr($valstr, 0, -2); $query = "INSERT INTO " . $tbl . " (" . $fldstr . ") VALUES (" . $valstr . ")"; - $this->db->query($query); + database::query($query); } } } @@ -150,21 +146,21 @@ abstract class table if (!isset($this->guid)) throw new Exception("GUID (null) does not exist"); - $guid = $this->db->esc($this->guid); + $guid = database::esc($this->guid); foreach ($this->fields as $tbl => $flds) { - $tbl = $this->db->esc($tbl); + $tbl = database::esc($tbl); $query = "DELETE FROM " . $tbl . " WHERE guid = '" . $guid . "'"; - $this->db->query($query); + database::query($query); } /* garbage collection */ $query = "DELETE FROM members WHERE guid = '" . $guid . "' OR member = '" . $guid . "'"; - $this->db->query($query); + database::query($query); $query = "DELETE FROM views WHERE guid = '" . $guid . "' OR viewer = '" . $guid . "'"; - $this->db->query($query); + database::query($query); } /* @@ -181,7 +177,7 @@ abstract class table private function getCurrentTimestamp() : string { $query = "SELECT now() AS stamp"; - $res = $this->db->query($query); + $res = database::query($query); return $res[0]['stamp']; } @@ -190,9 +186,9 @@ abstract class table */ private function isGUID(string $guid) : bool { - $guid = $this->db->esc($guid); + $guid = database::esc($guid); $query = "SELECT guid FROM objects WHERE guid = '" . $guid . "'"; - $res = $this->db->query($query); + $res = database::query($query); return count($res) > 0; } diff --git a/app/class/user.class.php b/app/class/user.class.php index fc969fa..45fa5a5 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -47,10 +47,10 @@ class user extends agent */ public static function getGuidByUname(string $uname) : ?string { - $uname = $this->db->esc($uname); + $uname = database::esc($uname); $query = "SELECT guid FROM objects WHERE objtype = 'user' AND name = '" . $uname . "'"; - $res = $this->db->query($query); + $res = database::query($query); if (count($res) == 0) return NULL; @@ -77,7 +77,7 @@ class user extends agent public static function getAll_ordByUname() : array { $query = "SELECT guid FROM objects WHERE objtype = 'user' ORDER BY name"; - $res = $this->db->query($query); + $res = database::query($query); $users = array(); @@ -95,7 +95,7 @@ class user extends agent { $query = "SELECT o.guid FROM objects o JOIN users u ON o.guid = u.guid " . "WHERE o.objtype = 'user' ORDER BY u.admin DESC, o.name"; - $res = $this->db->query($query); + $res = database::query($query); $users = array(); @@ -112,7 +112,7 @@ class user extends agent { $query = "SELECT o.guid FROM objects o JOIN users u ON o.guid = u.guid " . "WHERE o.objtype = 'user' AND u.admin = 1 ORDER BY o.name"; - $res = $this->db->query($query); + $res = database::query($query); $users = array(); -- cgit v1.2.3 From 861c98387001f99fe0e178d8202d2c3ec40538f0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 6 Feb 2017 02:28:20 -0500 Subject: Update function signatures for table class Various function (and their usages) in the table class have been updated to be static class function. --- app/class/table.class.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 0b9a53c..2759176 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -50,7 +50,7 @@ abstract class table { $guid = database::esc($guid); - if (!$this->isGUID($guid)) + if (!self::isGUID($guid)) throw new Exception("GUID " . $guid . " does not exist"); foreach ($this->fields as $tbl => $flds) @@ -77,7 +77,7 @@ abstract class table /* update existing object */ if (isset($this->guid)) { - $this->updated = $this->getCurrentTimestamp(); + $this->updated = self::getCurrentTimestamp(); foreach ($this->fields as $tbl => $flds) { @@ -105,8 +105,8 @@ abstract class table /* create new object */ else { - $this->guid = $this->getNewGUID(); - $this->created = $this->getCurrentTimestamp(); + $this->guid = self::getNewGUID(); + $this->created = self::getCurrentTimestamp(); $this->updated = $this->created; foreach ($this->fields as $tbl => $flds) @@ -166,7 +166,7 @@ abstract class table /* * Get a random sha256 blob, returned as a hexadecimal string */ - public function getBlob() : string + public static function getBlob() : string { return hash("sha256", openssl_random_pseudo_bytes(64)); } @@ -174,7 +174,7 @@ abstract class table /* * Get current timestamp as a string for object database purposes */ - private function getCurrentTimestamp() : string + private static function getCurrentTimestamp() : string { $query = "SELECT now() AS stamp"; $res = database::query($query); @@ -184,7 +184,7 @@ abstract class table /* * Check whether the given GUID exists */ - private function isGUID(string $guid) : bool + private static function isGUID(string $guid) : bool { $guid = database::esc($guid); $query = "SELECT guid FROM objects WHERE guid = '" . $guid . "'"; @@ -195,10 +195,10 @@ abstract class table /* * Get a new, unique GUID for a new system object */ - private function getNewGUID() : string + private static function getNewGUID() : string { - do $guid = substr($this->getBlob(), 0, 8); - while ($this->isGUID($guid)); + do $guid = substr(self::getBlob(), 0, 8); + while (self::isGUID($guid)); return $guid; } } -- cgit v1.2.3 From f8db4aae02465dabaf7907f5e821414eeeea14bf Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 7 Feb 2017 00:20:03 -0500 Subject: Add function expectType() to table class protected function exceptType added for use by subclasses to assert that the database object loaded is the correct type and to protect against cases like EG: passing the GUID for a group to new user(...); If a problem is detected, throw an exception. --- app/class/table.class.php | 15 +++++++++++++++ app/class/user.class.php | 1 + 2 files changed, 16 insertions(+) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 2759176..45be8c5 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -201,6 +201,21 @@ abstract class table while (self::isGUID($guid)); return $guid; } + + /* + * Assert that this object is of the expected type in the database. + * Throw an exception if a type mismatch exists. Check will only + * be performed if guid and objtype are set on this object. + */ + protected function expectType(string $objtype) : void + { + if (isset($this->guid) && isset($this->objtype)) + { + if ($this->objtype != $objtype) + throw new Exception("Invalid object allocation. " . $this->guid . " is a " . + $this->objtype . ", not a " . $objtype . "."); + } + } } ?> diff --git a/app/class/user.class.php b/app/class/user.class.php index 45fa5a5..7defa8f 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -38,6 +38,7 @@ class user extends agent ); parent::__construct($guid); + $this->expectType("user"); } /* -- cgit v1.2.3 From 912b28d44b340f9c418f3714d38df40e06f6f6da Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 13 Feb 2017 03:59:41 -0500 Subject: Add app-root(), app-path(), and similar function to globals.php --- app/class/globals.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 3512961..c936488 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -19,4 +19,38 @@ define("__VERSION__", "v0.0"); +/* + * Get the application root path. This is an absolute path on the server. + */ +function ar() : string +{ + return substr($_SERVER['SCRIPT_NAME'], 0, -10); // 10 = strlen of "/index.php" +} + +/* + * Get the current page's path. This is an absolute path on the server. + */ +function ap() : string +{ + return ar() . $_SERVER['PATH_INFO']; +} + +/* + * Redirect to the given URL and die. + */ +function redirect(string $url) : void +{ + header("Location: " . $url); + exit; +} + +/* + * Redirect to the given in-app URL and die. The given URL should be a path + * relative to the app root. + */ +function location(string $path) : void +{ + redirect(ar() . $path); +} + ?> -- cgit v1.2.3 From 30fc005d7cea626184ee57f3dea88881ad6cf6fa Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 13 Feb 2017 04:15:20 -0500 Subject: Implement function database::getInstance() --- app/class/database.class.php | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index c0f13d7..74587f5 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -16,6 +16,9 @@ define("DATABASE_CONFIG_FILE", "dbconfig.php"); is_file(DATABASE_CONFIG_FILE) && require_once DATABASE_CONFIG_FILE; +require_once "class/globals.php"; +require_once "class/mysql.class.php"; + /* * This class provides a common interface to various database drivers. * Scrott provides facilities for interacting with any DBMS that we can @@ -39,11 +42,36 @@ abstract class database /* * Return the database instance object, creating it if this is the * first call to this function. This function will need maintained - * as new DBMSs are supported. + * as new DBMSs are supported. This function will throw if the + * database is not configured. */ private static function getInstance() : database { - // TODO + global $_SCROTT; + + if (self::$instance) + return self::$instance; + + if (!isset($_SCROTT['conf'])) + throw new Exception("Scrott database configuration is missing."); + + switch ($_SCROTT['dbEngine']) + { + case "mysql": + $host = $_SCROTT['dbHost']; + $uname = $_SCROTT['dbUname']; + $passwd = $_SCROTT['dbPasswd']; + $dbname = $_SCROTT['dbName']; + self::$instance = new mysql($host, $uname, $passwd, $dbname); + break; + + default: + throw new Exception("Problem with Scrott database configuration. Invalid " . + "database engine specified."); + break; + } + + return self::$instance; } /* -- cgit v1.2.3 From a4bdf800e224a64fc704b6d19b668287f030a3e8 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 13 Feb 2017 04:23:49 -0500 Subject: Add function database::checkConfig() --- app/class/database.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index 74587f5..cdfdfce 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -98,6 +98,23 @@ abstract class database { return self::getInstance()->_esc($str); } + + /* + * Check whether Scrott's database config is loaded + */ + public static function checkConfig() : bool + { + try + { + $db = self::getInstance(); + } + catch (Exception $e) + { + return false; + } + + return true; + } } ?> -- cgit v1.2.3 From 093cca175e42c0d0040821b4687ebd5823e95a5f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 14 Feb 2017 23:42:06 -0500 Subject: Add function require_https() --- app/class/globals.php | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index c936488..8bfa029 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -53,4 +53,14 @@ function location(string $path) : void redirect(ar() . $path); } +/* + * Assert that the current connection to the server is over HTTPS. Redirect + * if not. + */ +function require_https() : void +{ + if (!isset($_SERVER['HTTPS'])) + redirect("https://" . $_SERVER['SERVER_NAME'] . ap()); +} + ?> -- cgit v1.2.3 From 009ff4a5b6c62239744102717136560b750e95c0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 15 Feb 2017 04:25:09 -0500 Subject: Add settings class --- app/class/settings.class.php | 94 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/class/settings.class.php (limited to 'app/class') diff --git a/app/class/settings.class.php b/app/class/settings.class.php new file mode 100644 index 0000000..5609605 --- /dev/null +++ b/app/class/settings.class.php @@ -0,0 +1,94 @@ + -- cgit v1.2.3 From 127a6bba72f699816f227164661e7b451a4e7e76 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 16 Feb 2017 00:18:33 -0500 Subject: Add functions for checking user/group permissions --- app/class/agent.class.php | 282 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 7c3b23c..52bfc1e 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -49,6 +49,288 @@ abstract class agent extends object return false; } + + /* + * Check whether this agent has access permission for given + * object + */ + public function canAccess(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj)) + return true; + + if ($obj->pubAcc) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canAccessSub($parent)) + return true; + } + else if ($this->owner) + { + $owner = new object($obj->owner); + if ($this->canAccessSub($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify permission for given + * object + */ + public function canModify(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membModify) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySub($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySub($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify members permission for + * given object + */ + public function canModifyMembers(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membMemb) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySubMembers($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySubMembers($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify permissions permission + * for given object + */ + public function canModifyPermissions(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySubPermissions($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySubPermissions($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has access-sub permission for + * given object + */ + public function canAccessSub(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membAccs) + return true; + + if ($obj->pubAccs) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canAccessSub($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canAccessSub($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has create-sub permission + * for given object + */ + public function canCreateSub(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membCres) + return true; + + if ($obj->pubCres) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canCreateSub($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canCreateSub($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify-sub permission + * for given object + */ + public function canModifySub(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membModifys) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySub($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySub($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify-sub-members + * permission for given object + */ + public function canModifySubMembers(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($this->isMemberOf($obj) && $obj->membMembs) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySubMembers($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySubMembers($owner)) + return true; + } + + return false; + } + + /* + * Check whether this agent has modify-sub-permissions + * permission for given object + */ + public function canModifySubPermissions(object $obj) : bool + { + if ($this->admin) + return true; + + if ($this->isOwnerOf($obj)) + return true; + + if ($obj->parent) + { + $parent = new object($obj->parent); + if ($this->canModifySubPermissions($parent)) + return true; + } + else if ($obj->owner) + { + $owner = new object($obj->owner); + if ($this->canModifySubPermissions($owner)) + return true; + } + + return false; + } } ?> -- cgit v1.2.3 From f4c10ec42537241fb77c93fa0eb1e31824c3aa73 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 25 Mar 2017 21:19:09 -0400 Subject: Add function user::initNew() --- app/class/user.class.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 7defa8f..b0b3435 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -173,6 +173,31 @@ class user extends agent } } + /* + * Initialize a new user object with the given username and plain + * text password. This function returns NULL if $uname is already + * being used. + */ + public static function initNew(string $uname, string $passwd) : ?user + { + if (self::getByUname($uname)) + return NULL; + + $user = new user(); + + /* if there exist no users already, make this new one an admin */ + if (count(self::getAll_ordByUname()) == 0) + $user->admin = 1; + + $user->name = $uname; + $user->objtype = "user"; + $user->setPasswd($passwd); + $user->setEmail(""); + $user->reg = 1; + + return $user; + } + /* * Get the salted and hashed form of a password */ -- cgit v1.2.3 From 173b002d3f9de83f93fc1a0128febcc44410c3d0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 04:14:58 -0400 Subject: Add function agent::getDisplayName() --- app/class/agent.class.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 52bfc1e..038c485 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -50,6 +50,24 @@ abstract class agent extends object return false; } + /* + * Get the display name for this agent. For groups this is the + * object name; for users, this is the object name, unless an + * alias is set. + */ + public function getDisplayName() : string + { + if ($this->objtype != "user") + return $this->name; + + $user = new user($this->guid); + + if ($user->alias != "") + return $user->alias; + + return $user->name; + } + /* * Check whether this agent has access permission for given * object -- cgit v1.2.3 From 0555c1a786144102fa1b9381f634138d2bd8c181 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 04:32:27 -0400 Subject: Add various helper functions for user class Added the function to verify and update the user's password. Added the function to confirm and update the user's email address. --- app/class/user.class.php | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index b0b3435..9892277 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -205,6 +205,51 @@ class user extends agent { return hash("sha256", $passwd . $salt); } + + /* + * Validate the given plain-text password for this user. Returns true if + * correct, false otherwise. + */ + public function validatePasswd(string $passwd) : bool + { + $auth = self::getAuth($passwd, $this->salt); + return $auth == $this->auth; + } + + /* + * Update the auth and salt for this user, given a new plain-text + * password. + */ + public function setPasswd(string $passwd) : void + { + $this->salt = self::getBlob(); + $this->auth = self::getAuth($passwd, $this->salt); + } + + /* + * Validate the email confirmation code for this user. Returns true if + * correct, false otherwise. On success, $this->emailConf is also set + * to 1 + */ + public function verifyEmail(string $ver) : bool + { + if ($ver != $this->emailVer) + return false; + + $this->emailConf = 1; + return true; + } + + /* + * Update the email address for this user. This function will automatically + * reset the emailConf flag and confirmation code for this user as well. + */ + public function setEmail(string $email) : void + { + $this->email = $email; + $this->emailVer = substr(self::getBlob(), 0, 8); + $this->emailConf = 0; + } } ?> -- cgit v1.2.3 From 69cc5d30f4e0446c504ce78152e2c305608b3866 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 05:47:34 -0400 Subject: Add functions for adding and removing object members Functions object::addMember() and object::remMember() --- app/class/object.class.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 6a77b37..8cd4603 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -84,6 +84,36 @@ class object extends table return $memb; } + + /* + * Add a user as a member of this object. Returns false if user is + * already a member, or if another error occurs; true otherwise. + */ + public function addMember(user $user) : bool + { + if ($user->isMemberOf($this) || !isset($user->guid)) + return false; + + $query = "INSERT INTO members (guid, member) VALUES ('" . database::esc($this->guid) . "', '" . + database::esc($user->guid) . "')"; + database::query($query); + return true; + } + + /* + * Remove a user as a member of this object. Returns false if user + * is not a member, or if another error occurs; true otherwise. + */ + public function remMember(user $user) : bool + { + if (!$user->isMemberOf($this) || !isset($user->guid)) + return false; + + $query = "DELETE FROM members WHERE guid = '" . database::esc($this->guid) . "' AND " . + "member = '" . database::esc($user->guid) . "'"; + database::query($query); + return true; + } } ?> -- cgit v1.2.3 From 5b8fafdc1608d017f63548a9af7c1043ccc61968 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 18:50:12 -0400 Subject: Rm unnecessary, circular requires from the object class file --- app/class/object.class.php | 3 --- 1 file changed, 3 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 8cd4603..f9fde02 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -13,9 +13,6 @@ */ require_once "class/table.class.php"; -require_once "class/agent.class.php"; -require_once "class/user.class.php"; -require_once "class/group.class.php"; /* * This is a generic database object. This is a supertype of all Scrott -- cgit v1.2.3 From 1ff545e5621ba711cd20aec06f4f39d645c2600a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 18:54:43 -0400 Subject: Add function object::typeOf() Also updated the getOwner function to use this instead of duplicating logic. --- app/class/object.class.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index f9fde02..ee3dc21 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -49,6 +49,15 @@ class object extends table parent::__construct($guid); } + /* + * Get the object type for the given GUID + */ + public static function typeOf(string $guid) : string + { + $obj = new object($guid); + return $obj->objtype; + } + /* * Get the owner of this object. Either a user object or a group * object will be returned. If this object does not have an owner, @@ -59,9 +68,7 @@ class object extends table if (!isset($this->owner) || $this->owner == "") return NULL; - $obj = new object($this->owner); - - if ($obj->objtype == "group") + if (self::typeOf($this->owner) == "group") return new group($this->owner); return new user($this->owner); -- cgit v1.2.3 From 356a3b04848b04eb2bb8be3c3fd457c492f9cb3f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 19:00:03 -0400 Subject: Add functions for object hierarchy mgmt Added to object class, functions: setOwner, getParent, setParent. --- app/class/object.class.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index ee3dc21..1fbe923 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -74,6 +74,34 @@ class object extends table return new user($this->owner); } + /* + * Update the owner of this object + */ + public function setOwner(agent $owner) : void + { + $this->owner = $owner->guid; + } + + /* + * Get the parent of this object. If this object does not have a + * parent, NULL will be returned. + */ + public function getParent() : ?object + { + if (!isset($this->parent) || $this->parent == "") + return NULL; + + return new object($this->parent); + } + + /* + * Update the parent of this object + */ + public function setParent(object $parent) : void + { + $this->parent = $parent->guid; + } + /* * Get an array of all members of this object */ -- cgit v1.2.3 From 5e8f3fb2adb74a9eb5c841848cfaae6502c9e91a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 19:18:19 -0400 Subject: Add group class --- app/class/group.class.php | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/class/group.class.php (limited to 'app/class') diff --git a/app/class/group.class.php b/app/class/group.class.php new file mode 100644 index 0000000..1e98aff --- /dev/null +++ b/app/class/group.class.php @@ -0,0 +1,66 @@ +fields['groups'] = array( + "guid", + ); + + parent::__construct($guid); + $this->expectType("group"); + } + + /* + * Get an array of all groups, sorted by name + */ + public static function getAll_ordByName() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'group' ORDER BY name"; + $res = database::query($query); + + $groups = array(); + + foreach ($res as $g) + $groups[] = new group($g['guid']); + + return $groups; + } + + /* + * Initialize a new group object with the given name and owner + */ + public static function initNew(string $name, user $owner) : group + { + $group = new group(); + $group->setOwner($owner); + $group->name = $name; + $group->objtype = "group"; + return $group; + } +} + +?> -- cgit v1.2.3 From 1ae7eab4711353b2144d0da40ac33270bc79a081 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 26 Mar 2017 22:52:30 -0400 Subject: Add function user::getGroups_ordByOwnByName() Lookup all groups a user owns or is a member of. --- app/class/user.class.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 9892277..97309f8 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -250,6 +250,34 @@ class user extends agent $this->emailVer = substr(self::getBlob(), 0, 8); $this->emailConf = 0; } + + /* + * Get all groups this user owns or is a member of. This isn't necessarily + * all groups this user cas access permissions for. Results are sorted by + * ownership, then by name. + */ + public function getGroups_ordByOwnByName() : array + { + $groups = array(); + + /* owner */ + $query = "SELECT guid FROM objects WHERE objtype = 'group' AND owner = '" . database::esc($this->guid) . "' " . + "ORDER BY name"; + $res = database::query($query); + + foreach ($res as $g) + $groups[] = new group($g['guid']); + + /* member */ + $query = "SELECT o.guid FROM objects o JOIN members m ON o.guid = m.guid WHERE o.objtype = 'group' AND " . + "m.member = '" . database::esc($this->guid) . "' ORDER BY o.name"; + $res = database::query($query); + + foreach ($res as $g) + $groups[] = new group($g['guid']); + + return $groups; + } } ?> -- cgit v1.2.3 From 9668df9d17f343bf8a130bd8b50a977485a926c8 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 29 Mar 2017 01:34:07 -0400 Subject: Add pad class --- app/class/pad.class.php | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 app/class/pad.class.php (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php new file mode 100644 index 0000000..7c53f30 --- /dev/null +++ b/app/class/pad.class.php @@ -0,0 +1,87 @@ +fields['pads'] = array( + "guid", + "stage", + "issueNumb", + ); + + parent::__construct($guid); + $this->expectType("pad"); + } + + /* + * Get an array of all pads, sorted by name + */ + public static function getAll_ordByName() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'pad' ORDER BY name"; + $res = database::query($query); + + $pads = array(); + + foreach ($res as $p) + $pads[] = new pad($p['guid']); + + return $pads; + } + + /* + * Get an array of all pads NOT owned by a group. These are root-level + * pads. Results are sorted by name. + */ + public static function getAllNoGroup_ordByName() : array + { + $query = "SELECT o.guid FROM objects o JOIN objects b ON o.owner = b.guid WHERE o.objtype = 'pad' AND " . + "b.objtype = 'user' ORDER BY o.name"; + $res = database::query($query); + + $pads = array(); + + foreach ($res as $p) + $pads[] = new pad($p['guid']); + + return $pads; + } + + /* + * Initialize a new pad object with the given name and owner + */ + public static function initNew(string $name, agent $owner) : pad + { + $pad = new pad(); + $pad->setOwner($owner); + $pad->name = $name; + $pad->objtype = "pad"; + $pad->issueNumb = 0; + return $pad; + } +} + +?> -- cgit v1.2.3 From 681861d9a85b9defc561d46af3f07b86efcaafdc Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 7 Apr 2017 21:01:47 -0400 Subject: Update function getParent() Now returns the correct type of object based on the original object's parent. --- app/class/object.class.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 1fbe923..dc988ae 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -91,7 +91,8 @@ class object extends table if (!isset($this->parent) || $this->parent == "") return NULL; - return new object($this->parent); + $parent = new object($this->parent); + return new $parent->objtype($parent->guid); } /* -- cgit v1.2.3 From 5340dce8ad70a8cfc62116144f668f22845a2f31 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 5 Apr 2017 22:31:50 -0400 Subject: Add stage class --- app/class/stage.class.php | 137 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 app/class/stage.class.php (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php new file mode 100644 index 0000000..31f6a94 --- /dev/null +++ b/app/class/stage.class.php @@ -0,0 +1,137 @@ +fields['stages'] = array( + "guid", + "stage", + ); + + parent::__construct($guid); + $this->expectType("stage"); + } + + /* + * Initialize a new stage object with the given name and parent + * pad. + */ + public static function initNew(string $name, pad $parent) : stage + { + $stage = new stage(); + $stage->setParent($parent); + $stage->name = $name; + $stage->objtype = "stage"; + return $stage; + } + + /* + * Get the stage following this one in the pipeline. NULL is + * returned if no stage is next. + */ + public function getNext() : ?stage + { + if (!isset($this->stage) || $this->stage == "") + return NULL; + + return new stage($this->stage); + } + + /* + * Get the stage preceding this one in the pipeline. NULL is + * returned if no stage is previous. + */ + public function getPrev() : ?stage + { + $pad = new pad($this->getParent()); + + if ($pad->stage == $this->guid) + return NULL; + + $query = "SELECT guid FROM stages WHERE stage = '" . database::esc($this->guid) . "'"; + $res = database::query($query); + + if (count($res) == 0) + return NULL; + + return new stage($res[0]['guid']); + } + + /* + * Get an array of all stages reachable from this one. The array + * is in proper order and includes the current stage. + */ + public function getArray() : array + { + $stages = array(); + $curr = $this; + + do $stages[] = $curr; + while (($curr = $curr->getNext())); + + return $stages; + } + + /* + * Reorder the stages of a pipeline by moving this one forward. + * This swaps the places of the current stage and the one following + * it. If this stage is the last in its pipeline, nothing is done + * and false is returned. + */ + public function moveForward() : bool + { + if (!($next = $this->getNext())) + return false; + + if (!($prev = $this->getPrev())) + $prev = new pad($this->getParent()); + + $tmp = $next->stage; + $prev->stage = $next->guid; + $next->stage = $this->guid; + $this->stage = $tmp; + + $next->saveObj(); + $prev->saveObj(); + return true; + } + + /* + * Reorder the stages of a pipeline by moving this one backward. + * This swaps the places of the current stage and the one preceding + * it. If this stage is the first in its pipeline, nothing is done + * and false is returned. + */ + public function moveBackward() : bool + { + if (!($prev = $this->getPrev())) + return false; + + return $prev->moveForward(); + } +} + +?> -- cgit v1.2.3 From 291b5a784e34099ece1839bf0bb3e8091c411576 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 7 Apr 2017 23:51:17 -0400 Subject: Update use of object getParent() function This function has been updated to construct and return the proper object type. This commit addresses the uses of this function so far to account for this. --- app/class/stage.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 31f6a94..150ac5d 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -66,7 +66,7 @@ class stage extends object */ public function getPrev() : ?stage { - $pad = new pad($this->getParent()); + $pad = $this->getParent(); if ($pad->stage == $this->guid) return NULL; @@ -107,7 +107,7 @@ class stage extends object return false; if (!($prev = $this->getPrev())) - $prev = new pad($this->getParent()); + $prev = $this->getParent(); $tmp = $next->stage; $prev->stage = $next->guid; -- cgit v1.2.3 From db7ad4f8943625e5c79fc09dae94a865f655493d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 8 Apr 2017 02:36:07 -0400 Subject: Fix bug in stage moveForward() function Since this function is used by moveBackward(), failing to call $this->saveObj() would result in changes made to $this being lost. $this is now written back to the db every time and calling saveObj() manually when using moveBackward() can result in bad data being written to the db. --- app/class/stage.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 150ac5d..2616bee 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -116,6 +116,7 @@ class stage extends object $next->saveObj(); $prev->saveObj(); + $this->saveObj(); return true; } -- cgit v1.2.3 From dd3aeb526b497ccde45e7ba018e963f2e249387a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 8 Apr 2017 23:00:56 -0400 Subject: Add stage function insertStage() --- app/class/stage.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 2616bee..1d21dcb 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -133,6 +133,17 @@ class stage extends object return $prev->moveForward(); } + + /* + * Insert a stage object in this pipeline, following $this object + */ + public function insertStage(stage $stage) : void + { + $stage->stage = $this->stage; + $this->stage = $stage->guid; + $stage->saveObj(); + $this->saveObj(); + } } ?> -- cgit v1.2.3 From 06e36d6b4009df47b0912c0f5b438171d153558f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 04:31:43 -0400 Subject: Add stage function getIssues_ordByDueByNumb() --- app/class/stage.class.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 1d21dcb..08a3708 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -95,6 +95,25 @@ class stage extends object return $stages; } + /* + * Get all issues in this stage, sorted by due date, then by + * issue number. + */ + public function getIssues_ordByDueByNumb() : array + { + $query = "SELECT o.guid FROM objects o JOIN issues i ON o.guid = i.guid " . + "WHERE o.objtype = 'issue' AND o.parent = '" . database::esc($this->guid) . + "' ORDER BY i.due, i.numb"; + $res = database::query($query); + + $issues = array(); + + foreach ($res as $i) + $issues[] = new issue($i['guid']); + + return $issues; + } + /* * Reorder the stages of a pipeline by moving this one forward. * This swaps the places of the current stage and the one following -- cgit v1.2.3 From 4cd8b4e9cac6ff104141cfb9154d11353cf9aab5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 04:44:09 -0400 Subject: Add stage function removeStage() --- app/class/stage.class.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 08a3708..1142bdd 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -163,6 +163,25 @@ class stage extends object $stage->saveObj(); $this->saveObj(); } + + /* + * Remove this stage object and move all of its issues. Issues are + * moved to the given stage object. Additionally, the pad may be + * given, in which case, those issues will be closed. + */ + public function removeStage(object $mvt) : void + { + if (!($prev = $this->getPrev())) + $prev = $this->getParent(); + + foreach ($this->getIssues_ordByDueByNumb() as $i) + $i->setParent($mvt); + + $prev->stage = $this->stage; + $prev->saveObj(); + + $this->delObj(); + } } ?> -- cgit v1.2.3 From d23eeb18e1604210dd55570c595fd184d8038f58 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 14:41:39 -0400 Subject: Add missing require --- app/class/stage.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 1142bdd..1825546 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -14,6 +14,7 @@ require_once "class/object.class.php"; require_once "class/pad.class.php"; +require_once "class/issue.class.php"; /* * This class models Scrott pad stages. Stages form a pipeline through -- cgit v1.2.3 From 478504986a348bb53765e137a6ea8293929954aa Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 15:01:54 -0400 Subject: Add pad function insertStage() --- app/class/pad.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php index 7c53f30..d062b0c 100644 --- a/app/class/pad.class.php +++ b/app/class/pad.class.php @@ -14,6 +14,7 @@ require_once "class/object.class.php"; require_once "class/agent.class.php"; +require_once "class/stage.class.php"; /* * This class models Scrott pads. Pads are the space for projects to track @@ -82,6 +83,17 @@ class pad extends object $pad->issueNumb = 0; return $pad; } + + /* + * Insert a stage object at the front of this pad's pipeline + */ + public function insertStage(stage $stage) : void + { + $stage->stage = $this->stage; + $this->stage = $stage->guid; + $stage->saveObj(); + $this->saveObj(); + } } ?> -- cgit v1.2.3 From 8a1a277c3cf747cbb82c9dcb660a925963f635a5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 17:48:27 -0400 Subject: Add pad function getStages() --- app/class/pad.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php index d062b0c..a5c771b 100644 --- a/app/class/pad.class.php +++ b/app/class/pad.class.php @@ -84,6 +84,16 @@ class pad extends object return $pad; } + /* + * Get an array of all stages under this pad. The array is in + * proper sequential order. + */ + public function getStages() : array + { + $stage = new stage($this->stage); + return $stage->getArray(); + } + /* * Insert a stage object at the front of this pad's pipeline */ -- cgit v1.2.3 From cc1f573a276bd5f022831f837bb9c1234df56ad9 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 20:08:05 -0400 Subject: Add agent function getPads_ordByOwnByName() --- app/class/agent.class.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 038c485..a2c8c2e 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -13,6 +13,7 @@ */ require_once "class/object.class.php"; +require_once "class/pad.class.php"; /* * This is a supertype for users and groups, since these two object types @@ -68,6 +69,35 @@ abstract class agent extends object return $user->name; } + /* + * Get all pads this agent owns or is a member of. This isn't + * necessarily all pads this agent has access permission for. + * Results are sorted by ownership, then by name. + */ + public function getPads_ordByOwnByName() : array + { + $pads = array(); + + /* owner */ + $query = "SELECT guid FROM objects WHERE objtype = 'pad' AND " . + "owner = '" . database::esc($this->guid) . "' ORDER BY name"; + $res = database::query($query); + + foreach ($res as $p) + $pads[] = new pad($p['guid']); + + /* members */ + $query = "SELECT o.guid FROM objects o JOIN members m ON " . + "o.guid = m.guid WHERE o.objtype = 'pad' AND " . + "m.member = '" . database::esc($this->guid) . "' ORDER BY o.name"; + $res = database::query($query); + + foreach ($res as $p) + $pads[] = new pad($p['guid']); + + return $pads; + } + /* * Check whether this agent has access permission for given * object -- cgit v1.2.3 From 3172d80347156b4d64061ad3eb1214e95fd0f7c4 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 23:15:20 -0400 Subject: Fix bugs in object class Added missing calls to function saveObj() where $this is mutated. --- app/class/object.class.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index dc988ae..14ab891 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -80,6 +80,7 @@ class object extends table public function setOwner(agent $owner) : void { $this->owner = $owner->guid; + $this->saveObj(); } /* @@ -101,6 +102,7 @@ class object extends table public function setParent(object $parent) : void { $this->parent = $parent->guid; + $this->saveObj(); } /* -- cgit v1.2.3 From ce1e026c42dbe142202d17675791579dfc30d6b5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 23:19:54 -0400 Subject: Fix bugs in user class Added calls to function saveObj() where $this is mutated. --- app/class/user.class.php | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 97309f8..a6addf6 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -194,6 +194,7 @@ class user extends agent $user->setPasswd($passwd); $user->setEmail(""); $user->reg = 1; + $user->saveObj(); return $user; } @@ -224,6 +225,7 @@ class user extends agent { $this->salt = self::getBlob(); $this->auth = self::getAuth($passwd, $this->salt); + $this->saveObj(); } /* @@ -237,6 +239,7 @@ class user extends agent return false; $this->emailConf = 1; + $this->saveObj(); return true; } @@ -249,6 +252,7 @@ class user extends agent $this->email = $email; $this->emailVer = substr(self::getBlob(), 0, 8); $this->emailConf = 0; + $this->saveObj(); } /* -- cgit v1.2.3 From f97e2dc2fe38ac26dde0ca18ba69bc2c412e699b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 23:22:03 -0400 Subject: Fix bug in group class --- app/class/group.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/group.class.php b/app/class/group.class.php index 1e98aff..5163cb8 100644 --- a/app/class/group.class.php +++ b/app/class/group.class.php @@ -59,6 +59,7 @@ class group extends agent $group->setOwner($owner); $group->name = $name; $group->objtype = "group"; + $group->saveObj(); return $group; } } -- cgit v1.2.3 From adf93dfd11483d69ad4bc823a2b936df270d951d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 23:25:25 -0400 Subject: Fix bug in pad class --- app/class/pad.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php index a5c771b..3966f58 100644 --- a/app/class/pad.class.php +++ b/app/class/pad.class.php @@ -81,6 +81,7 @@ class pad extends object $pad->name = $name; $pad->objtype = "pad"; $pad->issueNumb = 0; + $pad->saveObj(); return $pad; } -- cgit v1.2.3 From 003f48ae880ad61eec65d9816d485457c45c4b11 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 9 Apr 2017 23:30:40 -0400 Subject: Fix bug in stage class --- app/class/stage.class.php | 1 + 1 file changed, 1 insertion(+) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 1825546..760a9a2 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -46,6 +46,7 @@ class stage extends object $stage->setParent($parent); $stage->name = $name; $stage->objtype = "stage"; + $stage->saveObj(); return $stage; } -- cgit v1.2.3 From 602921ebd65b8a46190bbcf1eb7a83c409f2c926 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 13 Apr 2017 21:05:56 -0400 Subject: Add table function refreshObj() --- app/class/table.class.php | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 45be8c5..ac21aaf 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -67,6 +67,15 @@ abstract class table } } + /* + * This function uses loadObj to re-initialize this object, if in + * the case it gets modified in another scope. + */ + public function refreshObj() : void + { + $this->loadObj($this->guid); + } + /* * This function will update this object in the database, or insert new * data if this object does not yet have a GUID. This function uses the -- cgit v1.2.3 From 1b0d75037fa9c0b8e8f0265da348b2238a332931 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 13 Apr 2017 21:09:46 -0400 Subject: Update stage function moveBackward() Now calling refreshObj() to update altered pointers. --- app/class/stage.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 760a9a2..74c5f42 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -152,7 +152,9 @@ class stage extends object if (!($prev = $this->getPrev())) return false; - return $prev->moveForward(); + $ret = $prev->moveForward(); + $this->refreshObj(); + return $ret; } /* -- cgit v1.2.3 From e7228676ce51bf69cc974fc0bd8f8135c51fd036 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 13 Apr 2017 23:23:56 -0400 Subject: Fix bug in table function saveObj() While creating new objects, some default values (defined in the database schema) are missing in the PHP object definition. To address this, now while saving any *new* database objects, all undefined, expected fields are set to the empty string "". --- app/class/table.class.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index ac21aaf..c547e43 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -127,7 +127,10 @@ abstract class table foreach ($flds as $fld) { if (!isset($this->$fld)) + { + $this->$fld = ""; continue; + } $fld = database::esc($fld); $fldstr .= $fld . ", "; -- cgit v1.2.3 From 025a34dbd54a0886d766f386792310e5f6bfe701 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 14 Apr 2017 21:01:10 -0400 Subject: Revert "Fix bug in table function saveObj()" This reverts commit e7228676ce51bf69cc974fc0bd8f8135c51fd036. --- app/class/table.class.php | 3 --- 1 file changed, 3 deletions(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index c547e43..ac21aaf 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -127,10 +127,7 @@ abstract class table foreach ($flds as $fld) { if (!isset($this->$fld)) - { - $this->$fld = ""; continue; - } $fld = database::esc($fld); $fldstr .= $fld . ", "; -- cgit v1.2.3 From 453a2fc20886fa25b94017c1cccdbd05456a2d60 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 14 Apr 2017 20:50:24 -0400 Subject: Fix bug in table function saveObj() Added call to refreshObj() to the end of function saveObj() to fetch all default values defined by the database, not set on the object in PHP. --- app/class/table.class.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index ac21aaf..52a3e7d 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -142,6 +142,8 @@ abstract class table database::query($query); } } + + $this->refreshObj(); // fetch default, unset values from database } } -- cgit v1.2.3 From 2c9af1c3afad3f3aedef073a7436d677283ea456 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 16 Apr 2017 02:10:48 -0400 Subject: Add global state for page errors, warnings, etc. Added arrays to the global $_SCROTT variable to keep lists of page errors, warnings, and notices. Also added functions to globals.php for interacting with these arrays. --- app/class/globals.php | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 8bfa029..5a03da9 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -19,6 +19,19 @@ define("__VERSION__", "v0.0"); +/* + * These global variables are arrays of strings logged by Scrott business + * logic to report errors, warnings, or informational responses to the + * user in cases where an exception doesn't need to be thrown. + */ +define("ERROR", "errorlist"); +define("WARNING", "warninglist"); +define("NOTICE", "noticelist"); + +$_SCROTT[ERROR] = array(); +$_SCROTT[WARNING] = array(); +$_SCROTT[NOTICE] = array(); + /* * Get the application root path. This is an absolute path on the server. */ @@ -63,4 +76,31 @@ function require_https() : void redirect("https://" . $_SERVER['SERVER_NAME'] . ap()); } +/* + * Check for errors, warnings, or notices + */ +function isError(string $level) : bool +{ + global $_SCROTT; + return count($_SCROTT[$level]) > 0; +} + +/* + * Log an error, warning, or notice + */ +function logError(string $level, string $error) : void +{ + global $_SCROTT; + $_SCROTT[$level][] = $error; +} + +/* + * Get an array of all errors, warnings, or notices + */ +function getErrors(string $level) : array +{ + global $_SCROTT; + return $_SCROTT[$level]; +} + ?> -- cgit v1.2.3 From bbdbc8cbd9142c4377197964d48b2db5dfe00fa3 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 16 Apr 2017 03:17:09 -0400 Subject: Add global function saveFile() --- app/class/globals.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 5a03da9..615efa6 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -103,4 +103,45 @@ function getErrors(string $level) : array return $_SCROTT[$level]; } +/* + * Save an uploaded file and impose some constraints on supplied + * data. Caller can optionally pass some strings by reference to + * be given the supplied file's original name and mime-type. + * Maxsize is in bytes. If this function returns false, the + * appropriate error will be logged. + */ +function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = NULL, + ?string &$origName = NULL, ?string &$origMime = NULL) : bool +{ + if ($file['error'] > 0) + { + if ($file['error'] != UPLOAD_ERR_NO_FILE) + logError(ERROR, "An unknown error occurred"); + + return false; + } + + if ($file['size'] > $maxsize) + { + logError(ERROR, "File must be no larger than " . $maxsize . " bytes"); + return false; + } + + if (is_array($allowedMime) && array_search($file['type'], $allowedMime) === false) + { + logError(ERROR, "File type is not supported"); + return false; + } + + if (!move_uploaded_file($file['tmp_name'], $path)) + { + logError(ERROR, "Error saving uploaded file"); + return false; + } + + $origName = $file['name']; + $origMime = $file['type']; + return true; +} + ?> -- cgit v1.2.3 From 065c8bbb637954c47259e312ef87667e8d281497 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 00:29:19 -0400 Subject: Add image cropping logic User-supplied head images will need to be square in their dimensions, so that bootstrap displays them correctly. This function will crop image files after they are uploaded to the server. --- app/class/image.php | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 app/class/image.php (limited to 'app/class') diff --git a/app/class/image.php b/app/class/image.php new file mode 100644 index 0000000..6b73cae --- /dev/null +++ b/app/class/image.php @@ -0,0 +1,70 @@ + -- cgit v1.2.3 From a6a828521c61ebed3ec7038b2a53c002312a8bd0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 02:32:38 -0400 Subject: Add object function setHeadImg() --- app/class/object.class.php | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 14ab891..0dacd87 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -13,6 +13,7 @@ */ require_once "class/table.class.php"; +require_once "class/image.php"; /* * This is a generic database object. This is a supertype of all Scrott @@ -20,6 +21,16 @@ require_once "class/table.class.php"; */ class object extends table { + /* + * Constants used for uploading images + */ + public const HEAD_MAXSIZE = 1048576; // 1Mb + public const IMAGE_MIME = array( + "image/jpeg", + "image/jpg", + "image/png", + ); + /* * Constructor */ @@ -149,6 +160,27 @@ class object extends table database::query($query); return true; } + + /* + * Set the head image for this object, overwriting any existing + * image. $image should be an uploaded file to PHP, still + * unhandled. + */ + public function setHeadImg(array $image) : bool + { + $path = "dynmic/heads/" . $this->guid; + + if (!saveFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) + return false; + + if (!imageSquareCrop($path)) + { + $this->rmHeadImg(); + return false; + } + + return true; + } } ?> -- cgit v1.2.3 From ae94f42a59c1f308d973dc91214a63a1afb6cae3 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 21:47:52 -0400 Subject: Add object function rmHeadImg() --- app/class/object.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 0dacd87..55d0874 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -181,6 +181,18 @@ class object extends table return true; } + + /* + * Remove the head image for this object. This deletes the image + * on disk. + */ + public function rmHeadImg() : bool + { + if (!is_file("dynmic/heads/" . $this->guid)) + return true; + + return unlink("dynmic/heads/" . $this->guid); + } } ?> -- cgit v1.2.3 From 77d16dcad807e80e439d636301ccf588195f53fc Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 21:50:58 -0400 Subject: Add object function getHeadImg() --- app/class/object.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 55d0874..8d889e7 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -161,6 +161,14 @@ class object extends table return true; } + /* + * Get the URL to the head image resource for this object + */ + public function getHeadImg() : string + { + return ar() . "/df.php?d=heads&f=" . $this->guid; + } + /* * Set the head image for this object, overwriting any existing * image. $image should be an uploaded file to PHP, still -- cgit v1.2.3 From 3c4d7755d0a88c148884e0cbb90b327a96d87973 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 22:45:18 -0400 Subject: Add object function setBgImg() --- app/class/object.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 8d889e7..f8f5460 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -25,6 +25,7 @@ class object extends table * Constants used for uploading images */ public const HEAD_MAXSIZE = 1048576; // 1Mb + public const BG_MAXSIZE = 1048576; // 1Mb public const IMAGE_MIME = array( "image/jpeg", "image/jpg", @@ -201,6 +202,17 @@ class object extends table return unlink("dynmic/heads/" . $this->guid); } + + /* + * Set the background image for this object, overwriting any + * existing image. $image should be an uploaded file to PHP, + * still unhandled. + */ + public function setBgImg(array $image) : bool + { + $path = "dynmic/bgs/" . $this->guid; + return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); + } } ?> -- cgit v1.2.3 From 07a1a1b7f207101ad27084ad37082f96afad8ac6 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 22:47:00 -0400 Subject: Add object function rmBgImg() --- app/class/object.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index f8f5460..3bf64ec 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -213,6 +213,18 @@ class object extends table $path = "dynmic/bgs/" . $this->guid; return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); } + + /* + * Remove the background image for this object. This deletes + * the image on disk. + */ + public function rmBgImg() : bool + { + if (!is_file("dynmic/bgs/" . $this->guid)) + return true; + + return unlink("dynmic/bgs/" . $this->guid); + } } ?> -- cgit v1.2.3 From 8256cbd7976d463ddfd31660995540a9c8adc315 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Apr 2017 22:49:05 -0400 Subject: Add object function getBgImg() --- app/class/object.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 3bf64ec..7c80b5b 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -203,6 +203,18 @@ class object extends table return unlink("dynmic/heads/" . $this->guid); } + /* + * Get the URL to the background image resource for this + * object. If no image is set, NULL is returned. + */ + public function getBgImg() : ?string + { + if (!is_file("dynmic/bgs/" . $this->guid)) + return NULL; + + return ar() . "/df.php?d=bgs&f=" . $this->guid; + } + /* * Set the background image for this object, overwriting any * existing image. $image should be an uploaded file to PHP, -- cgit v1.2.3 From 37db482851a7724520e5db7aef89dcd8490cc081 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 1 Jun 2017 23:35:44 -0400 Subject: Add issue class --- app/class/issue.class.php | 117 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 app/class/issue.class.php (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php new file mode 100644 index 0000000..e439dff --- /dev/null +++ b/app/class/issue.class.php @@ -0,0 +1,117 @@ +fields['issues'] = array( + "guid", + "numb", + "assignee", + "seen", + "description", + "due", + "tags", + ); + + parent::__construct($guid); + $this->expectType("issue"); + } + + /* + * Initialize a new issue object with the given name, parent, and + * owner. + */ + public static function initNew(string $name, user $owner, stage $parent) : issue + { + $pad = $parent->getParent(); + $numb = $pad->issueNumb++; + $pad->saveObj(); + + $issue = new issue(); + $issue->setOwner($owner); + $issue->setParent($parent); + $issue->name = $name; + $issue->objtype = "issue"; + $issue->numb = $numb; + $issue->saveObj(); + return $issue; + } + + /* + * Get all activity for this issue. Messages are sorted by date + * created. + */ + public function getMesgs_ordByDatetime() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . + "parent = '" . database::esc($this->guid) . "' ORDER BY created"; + $res = database::query($query); + + $mesgs = array(); + + foreach ($res as $m) + $mesgs[] = new mesg($m['guid']); + + return $mesgs; + } + + /* + * Reset the seen flag and reassign this issue. + */ + public function assignTo(user $assignee) : void + { + $this->seen = 0; + $this->assignee = $assignee->guid; + $this->saveObj(); + } + + /* + * Advance this issue in the pipeline, closing it if already in the + * last stage. + */ + public function advance() : void + { + $stage = $this->getParent(); + + if (!($next = $stage->getNext())) + $this->close(); + + $this->setParent($next); + } + + /* + * Mark this issue as completed and closed. + */ + public function close() : void + { + $pad = $this->getParent()->getParent(); + $this->setParent($pad); + } +} + +?> -- cgit v1.2.3 From eb4b42f32856b0e5616b700614c8c68d77cd0ea3 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 3 Jun 2017 17:24:31 -0400 Subject: issue: Fix bug in function advance() The call to setParent() should have been in an else. It was being called every time... --- app/class/issue.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index e439dff..1286e1a 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -100,8 +100,8 @@ class issue extends object if (!($next = $stage->getNext())) $this->close(); - - $this->setParent($next); + else + $this->setParent($next); } /* -- cgit v1.2.3 From da90fc87384b1daa8104cdea14fb1b52f0f747b7 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 3 Jun 2017 17:31:16 -0400 Subject: issue: Fix bug in functions advance() and close() If the issue is already closed, these functions should do nothing. Continuing the logic in these functions could currupt the database. --- app/class/issue.class.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 1286e1a..3127a87 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -98,6 +98,9 @@ class issue extends object { $stage = $this->getParent(); + if ($stage->objtype != "stage") + return; + if (!($next = $stage->getNext())) $this->close(); else @@ -110,7 +113,9 @@ class issue extends object public function close() : void { $pad = $this->getParent()->getParent(); - $this->setParent($pad); + + if ($pad) + $this->setParent($pad); } } -- cgit v1.2.3 From 767f0b5559472828c09be7de50c80f017cc0540a Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 4 Jun 2017 00:51:21 -0400 Subject: mesg: Add mesg class --- app/class/mesg.class.php | 249 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 app/class/mesg.class.php (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php new file mode 100644 index 0000000..f9766ca --- /dev/null +++ b/app/class/mesg.class.php @@ -0,0 +1,249 @@ +fields['mesgs'] = array( + "guid", + "author", + "mesg", + "attachment", + ); + + parent::__construct($guid); + + try + { + $this->expectType("log"); + } + catch (Exception $e) + { + $this->expectType("mesg"); + } + } + + /* + * Initialize a new regular message. + */ + public static function initNew(string $message, user $author, object $parent) : mesg + { + $mesg = new mesg(); + $mesg->setOwner($author); + $mesg->setParent($parent); + $mesg->name = "message " . $mesg->guid; + $mesg->objtype = "mesg"; + $mesg->setAuthor($author); + $mesg->mesg = $message; + $mesg->saveObj(); + return $mesg; + } + + /* + * Initialize a new pad discussion thread. + */ + public static function initNewDiscussion(string $name, string $message, user $author, + pad $parent) : mesg + { + $mesg = new mesg(); + $mesg->setOwner($author); + $mesg->setParent($parent); + $mesg->name = $name; + $mesg->objtype = "mesg"; + $mesg->setAuthor($author); + $mesg->mesg = $message; + $mesg->saveObj(); + return $mesg; + } + + /* + * Initialize a new private message (user-to-user). + */ + public static function initNewPM(string $name, string $message, user $author, + user $rcpt) : mesg + { + $mesg = new mesg(); + $mesg->setOwner($rcpt); + $mesg->setParent($rcpt); + $mesg->name = $name; + $mesg->objtype = "mesg"; + $mesg->setAuthor($author); + $mesg->mesg = $message; + $mesg->saveObj(); + return $mesg; + } + + /* + * Initialize a new log message. + */ + public static function initNewLog(string $message, user $author, object $parent) : mesg + { + $owner = $parent->getOwner(); + + if (!$owner) + $owner = $parent; + + $mesg = new mesg(); + $mesg->setOwner($owner); + $mesg->setParent($parent); + $mesg->name = "log " . $mesg->guid; + $mesg->objtype = "log"; + $mesg->setAuthor($author); + $mesg->mesg = $message; + $mesg->saveObj(); + return $mesg; + } + + /* + * Initialize a new admin log message. + */ + public static function initNewAdminLog(string $message, user $author) : mesg + { + $mesg = new mesg(); + $mesg->saveObj(); // need to get a guid + $mesg->name = "admin log " . $mesg->guid; + $mesg->objtype = "log"; + $mesg->setAuthor($author); + $mesg->mesg = $message; + $mesg->saveObj(); + return $mesg; + } + + /* + * Get an array of messages in the admin log, between two timestamps. + * With no arguments, the entire log is returned. Results are sorted + * in reverse chronological order. + */ + public static function getAdminLog_ordByDatetime(?string $after = NULL, + ?string $before = NULL) : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'log' AND " . + "parent = '' "; + + if ($after) + $query += "AND created >= '" . database::esc($after) . "' "; + + if ($before) + $query += "AND created < '" . database::esc($before) . "' "; + + $query += "ORDER BY created DESC"; + $res = database::query($query); + + $log = array(); + + foreach ($res as $l) + $log[] = new mesg($l['guid']); + + return $log; + } + + /* + * Get the author of this message. This user either actually wrote + * this message, or caused it to be generated. + */ + public function getAuthor() : user + { + if (!isset($this->author) || $this->author == "") + return NULL; + + return new user($this->author); + } + + /* + * Set the author of this message. This should usually only be done + * while constructing a new message or to clear out references to + * a user that got removed. + */ + public function setAuthor(user $author) : void + { + $this->author = $author->guid; + $this->saveObj(); + } + + /* + * Check whether this message has been seen by the given user + */ + public function seenBy(user $viewer) : bool + { + $query = "SELECT * FROM views WHERE guid = '" . database::esc($this->guid) . "' " . + "AND viewer = '" . database::esc($viewer->guid) . "'"; + $res = database::query($query); + + return count($res) > 0; + } + + /* + * Mark this message as seen by the given user + */ + public function markSeen(user $viewer) : void + { + if (!$this->seenBy($viewer)) + { + $query = "INSERT INTO views (guid, viewer) VALUES ('" . + database::esc($this->guid) . "', '" . database::esc($viewer->guid) . "')"; + database::query($query); + } + } + + /* + * Format message (if auto generated), and insert HTML linebreaks (
) + * at message newlines. + */ + public function renderMesg() : string + { + if ($this->objtype == "log") + $mesg = sprintf($this->mesg, $this->getAuthor()->getDisplayName()); + else + $mesg = $this->mesg; + + return nl2br($mesg); + } + + /* + * Get the URL to this message's attachment. If no file is attached, + * NULL is returned. + */ + public function getAttachment() : ?string + { + if (!is_file("dynmic/attach/" . $this->guid)) + return NULL; + + return ar() . "/df.php?d=attach&f=" . $this->guid; + } + + /* + * Set the attached file for this message. Should typically be done + * during new message creation and not changed afterward. Returns + * false if there is a problem saving the attachment. + */ + public function setAttachment(array $file) : bool + { + $path = "dynmic/attach/" . $this->guid; + return saveFile($file, $path, self::ATTACH_MAXSIZE); + } +} + +?> -- cgit v1.2.3 From 4ba37714838e1f863c264d6bea7c7de75cd62514 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 4 Jun 2017 15:38:22 -0400 Subject: Define maxsize of attachment uploads Value set to 512 megabytes. --- app/class/mesg.class.php | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index f9766ca..fd541cd 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -22,6 +22,11 @@ require_once "class/pad.class.php"; */ class mesg extends object { + /* + * Constants used for uploading attachments + */ + public const ATTACH_MAXSIZE = 536870912; // 512Mb + /* * Constructor */ -- cgit v1.2.3 From d677fe73839b22fe2065be5a7f6c49a1b11e8c18 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 4 Jun 2017 15:44:13 -0400 Subject: Update mesg function setAttachment() Now saving the original name of the attachment file in the message object. --- app/class/mesg.class.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index fd541cd..5d6ba4d 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -247,7 +247,13 @@ class mesg extends object public function setAttachment(array $file) : bool { $path = "dynmic/attach/" . $this->guid; - return saveFile($file, $path, self::ATTACH_MAXSIZE); + $origName = ""; + + $ret = saveFile($file, $path, self::ATTACH_MAXSIZE, NULL, $origName); + $this->attachment = $origName; + $this->saveObj(); + + return $ret; } } -- cgit v1.2.3 From 8e9beac9888e894c8db46401b0d5b4c4cb18407d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 4 Jun 2017 20:25:35 -0400 Subject: Add mesg function makeIssue() This feature allows a pad-level discussion to be promoted to an issue. A new object is created, but all content is preserved. However, if the thread OP message had an attachment, that attachment cannot be retained. --- app/class/mesg.class.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index 5d6ba4d..6c84d4f 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -15,6 +15,8 @@ require_once "class/object.class.php"; require_once "class/user.class.php"; require_once "class/pad.class.php"; +require_once "class/stage.class.php"; +require_once "class/issue.class.php"; /* * This class models issue activity, private messaging, pad discussions, @@ -255,6 +257,31 @@ class mesg extends object return $ret; } + + /* + * Promote a pad discussion thread to an issue. This message object + * must be the top-level message (op) of the discussion thread (ie: + * its parent must be a pad). All reply messages to this one are + * retained and will be messages left on the new issue. A new issue + * object is created and this message object will be destroyed. If + * this is not an eligible message for promotion, NULL is returned. + */ + public function makeIssue(stage $parent) : ?issue + { + if ($this->getParent()->objtype != "pad") + return NULL; + + $issue = issue::initNew($this->name, $this->getOwner(), $parent); + $issue->created = $this->created; + $issue->description = $this->mesg; + $issue->saveObj(); + + foreach ($this->getMesgs_ordByDatetime() as $mesg) + $mesg->setParent($issue); + + $this->delObj(); + return $issue; + } } ?> -- cgit v1.2.3 From a8e79321426b1436f46ba5b7c5dee390c94bdb8b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 4 Jun 2017 20:57:12 -0400 Subject: Move function getMesgs() into object class This function is needed in the scope of issue, mesgs, and pads alike. It would also make sense to use this to retrive users' private messages. For these reasons, this function is now being defined higher up in the object hierarchy. --- app/class/issue.class.php | 18 ------------------ app/class/object.class.php | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 3127a87..6056457 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -62,24 +62,6 @@ class issue extends object return $issue; } - /* - * Get all activity for this issue. Messages are sorted by date - * created. - */ - public function getMesgs_ordByDatetime() : array - { - $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . - "parent = '" . database::esc($this->guid) . "' ORDER BY created"; - $res = database::query($query); - - $mesgs = array(); - - foreach ($res as $m) - $mesgs[] = new mesg($m['guid']); - - return $mesgs; - } - /* * Reset the seen flag and reassign this issue. */ diff --git a/app/class/object.class.php b/app/class/object.class.php index 7c80b5b..461f1b1 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -162,6 +162,24 @@ class object extends table return true; } + /* + * Get all messages on this object. Messages are sorted by date + * created. + */ + public function getMesgs_ordByDatetime() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . + "parent = '" . database::esc($this->guid) . "' ORDER BY created"; + $res = database::query($query); + + $mesgs = array(); + + foreach ($res as $m) + $mesgs[] = new mesg($m['guid']); + + return $mesgs; + } + /* * Get the URL to the head image resource for this object */ -- cgit v1.2.3 From dce33112a2e2a73acd08ee0dc80ca567f34c7065 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 29 May 2017 00:59:09 -0400 Subject: Add SMTP configuration variables --- app/class/settings.class.php | 50 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'app/class') diff --git a/app/class/settings.class.php b/app/class/settings.class.php index 5609605..abafd0a 100644 --- a/app/class/settings.class.php +++ b/app/class/settings.class.php @@ -89,6 +89,56 @@ abstract class settings { return self::option("allowPublicSignup", false, $value); } + + /* + * SMTP email address + */ + public static function smtpEmailAddress(?string $value = NULL) : string + { + return self::option("smtpEmailAddress", "", $value); + } + + /* + * SMTP server address + */ + public static function smtpServer(?string $value = NULL) : string + { + return self::option("smtpServer", "", $value); + } + + /* + * SMTP port number + */ + public static function smtpPort(?int $value = NULL) : int + { + return self::option("smtpPort", 0, $value); + } + + /* + * SMTP security + * Should be '', 'ssl', or 'tls'. See the 'SMTPSecure' property + * from PHP Mailer module. + */ + public static function smtpSecurity(?string $value = NULL) : string + { + return self::option("smtpSecurity", "", $value); + } + + /* + * SMTP username + */ + public static function smtpUname(?string $value = NULL) : string + { + return self::option("smtpUname", "", $value); + } + + /* + * SMTP password + */ + public static function smtpPasswd(?string $value = NULL) : string + { + return self::option("smtpPasswd", "", $value); + } } ?> -- cgit v1.2.3 From 8d382d0d86da37c9019de1717a75dc6e003f6388 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 29 May 2017 01:08:41 -0400 Subject: Change default SMTP port to 25 --- app/class/settings.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/settings.class.php b/app/class/settings.class.php index abafd0a..5b7cdeb 100644 --- a/app/class/settings.class.php +++ b/app/class/settings.class.php @@ -111,7 +111,7 @@ abstract class settings */ public static function smtpPort(?int $value = NULL) : int { - return self::option("smtpPort", 0, $value); + return self::option("smtpPort", 25, $value); } /* -- cgit v1.2.3 From a71aba8c06b829b593b876a53e0c8bff2d343e27 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 29 May 2017 03:22:11 -0400 Subject: Add PHPMailer v5.2.23 --- app/class/phpmailer.class.php | 4039 +++++++++++++++++++++++++++++++++++++++++ app/class/smtp.class.php | 1251 +++++++++++++ 2 files changed, 5290 insertions(+) create mode 100644 app/class/phpmailer.class.php create mode 100644 app/class/smtp.class.php (limited to 'app/class') diff --git a/app/class/phpmailer.class.php b/app/class/phpmailer.class.php new file mode 100644 index 0000000..1b31ec1 --- /dev/null +++ b/app/class/phpmailer.class.php @@ -0,0 +1,4039 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailer - PHP email creation and transport class. + * @package PHPMailer + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer +{ + /** + * The PHPMailer Version number. + * @var string + */ + public $Version = '5.2.23'; + + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * @var integer + */ + public $Priority = null; + + /** + * The character set of the message. + * @var string + */ + public $CharSet = 'iso-8859-1'; + + /** + * The MIME Content-type of the message. + * @var string + */ + public $ContentType = 'text/plain'; + + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * @var string + */ + public $Encoding = '8bit'; + + /** + * Holds the most recent mailer error message. + * @var string + */ + public $ErrorInfo = ''; + + /** + * The From email address for the message. + * @var string + */ + public $From = 'root@localhost'; + + /** + * The From name of the message. + * @var string + */ + public $FromName = 'Root User'; + + /** + * The Sender email (Return-Path) of the message. + * If not empty, will be sent via -f to sendmail or as 'MAIL FROM' in smtp mode. + * @var string + */ + public $Sender = ''; + + /** + * The Return-Path of the message. + * If empty, it will be set to either From or Sender. + * @var string + * @deprecated Email senders should never set a return-path header; + * it's the receiver's job (RFC5321 section 4.4), so this no longer does anything. + * @link https://tools.ietf.org/html/rfc5321#section-4.4 RFC5321 reference + */ + public $ReturnPath = ''; + + /** + * The Subject of the message. + * @var string + */ + public $Subject = ''; + + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * @var string + */ + public $Body = ''; + + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * @var string + */ + public $AltBody = ''; + + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal events, use the bundled extras/EasyPeasyICS.php class or iCalcreator + * @link http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ + * @link http://kigkonsult.se/iCalcreator/ + * @var string + */ + public $Ical = ''; + + /** + * The complete compiled MIME message body. + * @access protected + * @var string + */ + protected $MIMEBody = ''; + + /** + * The complete compiled MIME message headers. + * @var string + * @access protected + */ + protected $MIMEHeader = ''; + + /** + * Extra headers that createHeader() doesn't fold in. + * @var string + * @access protected + */ + protected $mailHeader = ''; + + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * @var integer + */ + public $WordWrap = 0; + + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * @var string + */ + public $Mailer = 'mail'; + + /** + * The path to the sendmail program. + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * @var boolean + */ + public $UseSendmailOptions = true; + + /** + * Path to PHPMailer plugins. + * Useful if the SMTP class is not in the PHP include path. + * @var string + * @deprecated Should not be needed now there is an autoloader. + */ + public $PluginDir = ''; + + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * @var string + */ + public $ConfirmReadingTo = ''; + + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * @var string + */ + public $Hostname = ''; + + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * You can set your own, but it must be in the format "", + * as defined in RFC5322 section 3.6.4 or it will be ignored. + * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 + * @var string + */ + public $MessageID = ''; + + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * @var string + */ + public $MessageDate = ''; + + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * @var string + */ + public $Host = 'localhost'; + + /** + * The default SMTP server port. + * @var integer + * @TODO Why is this needed when the SMTP class takes care of it? + */ + public $Port = 25; + + /** + * The SMTP HELO of the message. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * @var string + * @see PHPMailer::$Hostname + */ + public $Helo = ''; + + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls' + * @var string + */ + public $SMTPSecure = ''; + + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * @var boolean + */ + public $SMTPAutoTLS = true; + + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * @var boolean + * @see PHPMailer::$Username + * @see PHPMailer::$Password + */ + public $SMTPAuth = false; + + /** + * Options array passed to stream_context_create when connecting via SMTP. + * @var array + */ + public $SMTPOptions = array(); + + /** + * SMTP username. + * @var string + */ + public $Username = ''; + + /** + * SMTP password. + * @var string + */ + public $Password = ''; + + /** + * SMTP auth type. + * Options are CRAM-MD5, LOGIN, PLAIN, NTLM, XOAUTH2, attempted in that order if not specified + * @var string + */ + public $AuthType = ''; + + /** + * SMTP realm. + * Used for NTLM auth + * @var string + */ + public $Realm = ''; + + /** + * SMTP workstation. + * Used for NTLM auth + * @var string + */ + public $Workstation = ''; + + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer + */ + public $Timeout = 300; + + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output + * @var integer + * @see SMTP::$do_debug + */ + public $SMTPDebug = 0; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable + * @see SMTP::$Debugoutput + */ + public $Debugoutput = 'echo'; + + /** + * Whether to keep SMTP connection open after each message. + * If this is set to true then to close the connection + * requires an explicit call to smtpClose(). + * @var boolean + */ + public $SMTPKeepAlive = false; + + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * @var boolean + */ + public $SingleTo = false; + + /** + * Storage for addresses when SingleTo is enabled. + * @var array + * @TODO This should really not be public + */ + public $SingleToArray = array(); + + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * @link https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Postfix VERP info + * @var boolean + */ + public $do_verp = false; + + /** + * Whether to allow sending messages with an empty body. + * @var boolean + */ + public $AllowEmpty = false; + + /** + * The default line ending. + * @note The default remains "\n". We force CRLF where we know + * it must be used via self::CRLF. + * @var string + */ + public $LE = "\n"; + + /** + * DKIM selector. + * @var string + */ + public $DKIM_selector = ''; + + /** + * DKIM Identity. + * Usually the email address used as the source of the email. + * @var string + */ + public $DKIM_identity = ''; + + /** + * DKIM passphrase. + * Used if your key is encrypted. + * @var string + */ + public $DKIM_passphrase = ''; + + /** + * DKIM signing domain name. + * @example 'example.com' + * @var string + */ + public $DKIM_domain = ''; + + /** + * DKIM private key file path. + * @var string + */ + public $DKIM_private = ''; + + /** + * DKIM private key string. + * If set, takes precedence over `$DKIM_private`. + * @var string + */ + public $DKIM_private_string = ''; + + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * boolean $result result of the send action + * string $to email address of the recipient + * string $cc cc email addresses + * string $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * @var string + */ + public $action_function = ''; + + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace for none, or a string to use + * @var string + */ + public $XMailer = ''; + + /** + * Which validator to use by default when validating email addresses. + * May be a callable to inject your own validator, but there are several built-in validators. + * @see PHPMailer::validateAddress() + * @var string|callable + * @static + */ + public static $validator = 'auto'; + + /** + * An instance of the SMTP sender class. + * @var SMTP + * @access protected + */ + protected $smtp = null; + + /** + * The array of 'to' names and addresses. + * @var array + * @access protected + */ + protected $to = array(); + + /** + * The array of 'cc' names and addresses. + * @var array + * @access protected + */ + protected $cc = array(); + + /** + * The array of 'bcc' names and addresses. + * @var array + * @access protected + */ + protected $bcc = array(); + + /** + * The array of reply-to names and addresses. + * @var array + * @access protected + */ + protected $ReplyTo = array(); + + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + */ + protected $all_recipients = array(); + + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + */ + protected $RecipientsQueue = array(); + + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$ReplyTo + */ + protected $ReplyToQueue = array(); + + /** + * The array of attachments. + * @var array + * @access protected + */ + protected $attachment = array(); + + /** + * The array of custom headers. + * @var array + * @access protected + */ + protected $CustomHeader = array(); + + /** + * The most recent Message-ID (including angular brackets). + * @var string + * @access protected + */ + protected $lastMessageID = ''; + + /** + * The message's MIME type. + * @var string + * @access protected + */ + protected $message_type = ''; + + /** + * The array of MIME boundary strings. + * @var array + * @access protected + */ + protected $boundary = array(); + + /** + * The array of available languages. + * @var array + * @access protected + */ + protected $language = array(); + + /** + * The number of errors encountered. + * @var integer + * @access protected + */ + protected $error_count = 0; + + /** + * The S/MIME certificate file path. + * @var string + * @access protected + */ + protected $sign_cert_file = ''; + + /** + * The S/MIME key file path. + * @var string + * @access protected + */ + protected $sign_key_file = ''; + + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * @var string + * @access protected + */ + protected $sign_extracerts_file = ''; + + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * @var string + * @access protected + */ + protected $sign_key_pass = ''; + + /** + * Whether to throw exceptions for errors. + * @var boolean + * @access protected + */ + protected $exceptions = false; + + /** + * Unique ID used for message ID and boundaries. + * @var string + * @access protected + */ + protected $uniqueid = ''; + + /** + * Error severity: message only, continue processing. + */ + const STOP_MESSAGE = 0; + + /** + * Error severity: message, likely ok to continue processing. + */ + const STOP_CONTINUE = 1; + + /** + * Error severity: message, plus full stop, critical error reached. + */ + const STOP_CRITICAL = 2; + + /** + * SMTP RFC standard line ending. + */ + const CRLF = "\r\n"; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer + */ + const MAX_LINE_LENGTH = 998; + + /** + * Constructor. + * @param boolean $exceptions Should we throw external exceptions? + */ + public function __construct($exceptions = null) + { + if ($exceptions !== null) { + $this->exceptions = (boolean)$exceptions; + } + } + + /** + * Destructor. + */ + public function __destruct() + { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do) + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string $params Params + * @access private + * @return boolean + */ + private function mailPassthru($to, $subject, $body, $header, $params) + { + //Check overloading of mail function to avoid double-encoding + if (ini_get('mbstring.func_overload') & 1) { + $subject = $this->secureHeader($subject); + } else { + $subject = $this->encodeHeader($this->secureHeader($subject)); + } + + //Can't use additional_parameters in safe_mode, calling mail() with null params breaks + //@link http://php.net/manual/en/function.mail.php + if (ini_get('safe_mode') or !$this->UseSendmailOptions or is_null($params)) { + $result = @mail($to, $subject, $body, $header); + } else { + $result = @mail($to, $subject, $body, $header, $params); + } + return $result; + } + /** + * Output debugging info via user-defined method. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * @param string $str + */ + protected function edebug($str) + { + if ($this->SMTPDebug <= 0) { + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $this->SMTPDebug); + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ) + . "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/\r\n?/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( + "\n", + "\n \t ", + trim($str) + ) . "\n"; + } + } + + /** + * Sets message type to HTML or plain. + * @param boolean $isHtml True for HTML mode. + * @return void + */ + public function isHTML($isHtml = true) + { + if ($isHtml) { + $this->ContentType = 'text/html'; + } else { + $this->ContentType = 'text/plain'; + } + } + + /** + * Send messages using SMTP. + * @return void + */ + public function isSMTP() + { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + * @return void + */ + public function isMail() + { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + * @return void + */ + public function isSendmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'sendmail')) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + * @return void + */ + public function isQmail() + { + $ini_sendmail_path = ini_get('sendmail_path'); + + if (!stristr($ini_sendmail_path, 'qmail')) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * @param string $address The email address to send to + * @param string $name + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addAddress($address, $name = '') + { + return $this->addOrEnqueueAnAddress('to', $address, $name); + } + + /** + * Add a "CC" address. + * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. + * @param string $address The email address to send to + * @param string $name + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('cc', $address, $name); + } + + /** + * Add a "BCC" address. + * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. + * @param string $address The email address to send to + * @param string $name + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addBCC($address, $name = '') + { + return $this->addOrEnqueueAnAddress('bcc', $address, $name); + } + + /** + * Add a "Reply-To" address. + * @param string $address The email address to reply to + * @param string $name + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addReplyTo($address, $name = '') + { + return $this->addOrEnqueueAnAddress('Reply-To', $address, $name); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addOrEnqueueAnAddress($kind, $address, $name) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + if (($pos = strrpos($address, '@')) === false) { + // At-sign is misssing. + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + $params = array($kind, $address, $name); + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ($this->has8bitChars(substr($address, ++$pos)) and $this->idnSupported()) { + if ($kind != 'Reply-To') { + if (!array_key_exists($address, $this->RecipientsQueue)) { + $this->RecipientsQueue[$address] = $params; + return true; + } + } else { + if (!array_key_exists($address, $this->ReplyToQueue)) { + $this->ReplyToQueue[$address] = $params; + return true; + } + } + return false; + } + // Immediately add standard addresses without IDN. + return call_user_func_array(array($this, 'addAnAddress'), $params); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addAnAddress($kind, $address, $name = '') + { + if (!in_array($kind, array('to', 'cc', 'bcc', 'Reply-To'))) { + $error_message = $this->lang('Invalid recipient kind: ') . $kind; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + if (!$this->validateAddress($address)) { + $error_message = $this->lang('invalid_address') . " (addAnAddress $kind): $address"; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + if ($kind != 'Reply-To') { + if (!array_key_exists(strtolower($address), $this->all_recipients)) { + array_push($this->$kind, array($address, $name)); + $this->all_recipients[strtolower($address)] = true; + return true; + } + } else { + if (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = array($address, $name); + return true; + } + } + return false; + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * @return array + * @link http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + */ + public function parseAddresses($addrstr, $useimap = true) + { + $addresses = array(); + if ($useimap and function_exists('imap_rfc822_parse_adrlist')) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist($addrstr, ''); + foreach ($list as $address) { + if ($address->host != '.SYNTAX-ERROR.') { + if ($this->validateAddress($address->mailbox . '@' . $address->host)) { + $addresses[] = array( + 'name' => (property_exists($address, 'personal') ? $address->personal : ''), + 'address' => $address->mailbox . '@' . $address->host + ); + } + } + } + } else { + //Use this simpler parser + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if ($this->validateAddress($address)) { + $addresses[] = array( + 'name' => '', + 'address' => $address + ); + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + if ($this->validateAddress($email)) { + $addresses[] = array( + 'name' => trim(str_replace(array('"', "'"), '', $name)), + 'address' => $email + ); + } + } + } + } + return $addresses; + } + + /** + * Set the From and FromName properties. + * @param string $address + * @param string $name + * @param boolean $auto Whether to also set the Sender address, defaults to true + * @throws phpmailerException + * @return boolean + */ + public function setFrom($address, $name = '', $auto = true) + { + $address = trim($address); + $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim + // Don't validate now addresses with IDN. Will be done in send(). + if (($pos = strrpos($address, '@')) === false or + (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and + !$this->validateAddress($address)) { + $error_message = $this->lang('invalid_address') . " (setFrom) $address"; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + $this->From = $address; + $this->FromName = $name; + if ($auto) { + if (empty($this->Sender)) { + $this->Sender = $address; + } + } + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * @return string + */ + public function getLastMessageID() + { + return $this->lastMessageID; + } + + /** + * Check that a string looks like an email address. + * @param string $address The email address to check + * @param string|callable $patternselect A selector for the validation pattern to use : + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * Alternatively you may pass in a callable to inject your own validator, for example: + * PHPMailer::validateAddress('user@example.com', function($address) { + * return (strpos($address, '@') !== false); + * }); + * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator. + * @return boolean + * @static + * @access public + */ + public static function validateAddress($address, $patternselect = null) + { + if (is_null($patternselect)) { + $patternselect = self::$validator; + } + if (is_callable($patternselect)) { + return call_user_func($patternselect, $address); + } + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) { + return false; + } + if (!$patternselect or $patternselect == 'auto') { + //Check this constant first so it works when extension_loaded() is disabled by safe mode + //Constant was added in PHP 5.2.4 + if (defined('PCRE_VERSION')) { + //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2 + if (version_compare(PCRE_VERSION, '8.0.3') >= 0) { + $patternselect = 'pcre8'; + } else { + $patternselect = 'pcre'; + } + } elseif (function_exists('extension_loaded') and extension_loaded('pcre')) { + //Fall back to older PCRE + $patternselect = 'pcre'; + } else { + //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension + if (version_compare(PHP_VERSION, '5.2.0') >= 0) { + $patternselect = 'php'; + } else { + $patternselect = 'noregex'; + } + } + } + switch ($patternselect) { + case 'pcre8': + /** + * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains. + * @link http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (boolean)preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'pcre': + //An older regex that doesn't need a recent PCRE + return (boolean)preg_match( + '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' . + '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' . + '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' . + '@(?>(?![a-z0-9-]{64,})(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>\.(?![a-z0-9-]{64,})' . + '(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)){0,126}|\[(?:(?>IPv6:(?>(?>[a-f0-9]{1,4})(?>:' . + '[a-f0-9]{1,4}){7}|(?!(?:.*[a-f0-9][:\]]){8,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?' . + '::(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?))|(?>(?>IPv6:(?>[a-f0-9]{1,4}(?>:' . + '[a-f0-9]{1,4}){5}:|(?!(?:.*[a-f0-9]:){6,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4})?' . + '::(?>(?:[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4}):)?))?(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD', + $address + ); + case 'html5': + /** + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (boolean)preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'noregex': + //No PCRE! Do something _very_ approximate! + //Check the address is 3 chars or longer and contains an @ that's not the first or last char + return (strlen($address) >= 3 + and strpos($address, '@') >= 1 + and strpos($address, '@') != strlen($address) - 1); + case 'php': + default: + return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL); + } + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * "intl" and "mbstring" PHP extensions. + * @return bool "true" if required functions for IDN support are present + */ + public function idnSupported() + { + // @TODO: Write our own "idn_to_ascii" function for PHP <= 5.2. + return function_exists('idn_to_ascii') and function_exists('mb_convert_encoding'); + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain has characters not allowed in an IDN) + * @see PHPMailer::$CharSet + * @param string $address The email address to convert + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress($address) + { + // Verify we have required functions, CharSet, and at-sign. + if ($this->idnSupported() and + !empty($this->CharSet) and + ($pos = strrpos($address, '@')) !== false) { + $domain = substr($address, ++$pos); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ($this->has8bitChars($domain) and @mb_check_encoding($domain, $this->CharSet)) { + $domain = mb_convert_encoding($domain, 'UTF-8', $this->CharSet); + if (($punycode = defined('INTL_IDNA_VARIANT_UTS46') ? + idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46) : + idn_to_ascii($domain)) !== false) { + return substr($address, 0, $pos) . $punycode; + } + } + } + return $address; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * @throws phpmailerException + * @return boolean false on error - See the ErrorInfo property for details of the error. + */ + public function send() + { + try { + if (!$this->preSend()) { + return false; + } + return $this->postSend(); + } catch (phpmailerException $exc) { + $this->mailHeader = ''; + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + return false; + } + } + + /** + * Prepare a message for sending. + * @throws phpmailerException + * @return boolean + */ + public function preSend() + { + try { + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) { + $params[1] = $this->punyencodeAddress($params[1]); + call_user_func_array(array($this, 'addAnAddress'), $params); + } + if ((count($this->to) + count($this->cc) + count($this->bcc)) < 1) { + throw new phpmailerException($this->lang('provide_address'), self::STOP_CRITICAL); + } + + // Validate From, Sender, and ConfirmReadingTo addresses + foreach (array('From', 'Sender', 'ConfirmReadingTo') as $address_kind) { + $this->$address_kind = trim($this->$address_kind); + if (empty($this->$address_kind)) { + continue; + } + $this->$address_kind = $this->punyencodeAddress($this->$address_kind); + if (!$this->validateAddress($this->$address_kind)) { + $error_message = $this->lang('invalid_address') . ' (punyEncode) ' . $this->$address_kind; + $this->setError($error_message); + $this->edebug($error_message); + if ($this->exceptions) { + throw new phpmailerException($error_message); + } + return false; + } + } + + // Set whether the message is multipart/alternative + if ($this->alternativeExists()) { + $this->ContentType = 'multipart/alternative'; + } + + $this->setMessageType(); + // Refuse to send an empty message unless we are specifically allowing it + if (!$this->AllowEmpty and empty($this->Body)) { + throw new phpmailerException($this->lang('empty_message'), self::STOP_CRITICAL); + } + + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + // To capture the complete message when using mail(), create + // an extra header list which createHeader() doesn't fold in + if ($this->Mailer == 'mail') { + if (count($this->to) > 0) { + $this->mailHeader .= $this->addrAppend('To', $this->to); + } else { + $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader($this->secureHeader(trim($this->Subject))) + ); + } + + // Sign with DKIM if enabled + if (!empty($this->DKIM_domain) + && !empty($this->DKIM_selector) + && (!empty($this->DKIM_private_string) + || (!empty($this->DKIM_private) && file_exists($this->DKIM_private)) + ) + ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader($this->secureHeader($this->Subject)), + $this->MIMEBody + ); + $this->MIMEHeader = rtrim($this->MIMEHeader, "\r\n ") . self::CRLF . + str_replace("\r\n", "\n", $header_dkim) . self::CRLF; + } + return true; + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + return false; + } + } + + /** + * Actually send a message. + * Send the email via the selected mechanism + * @throws phpmailerException + * @return boolean + */ + public function postSend() + { + try { + // Choose the mailer and send through it + switch ($this->Mailer) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody); + case 'smtp': + return $this->smtpSend($this->MIMEHeader, $this->MIMEBody); + case 'mail': + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + default: + $sendMethod = $this->Mailer.'Send'; + if (method_exists($this, $sendMethod)) { + return $this->$sendMethod($this->MIMEHeader, $this->MIMEBody); + } + + return $this->mailSend($this->MIMEHeader, $this->MIMEBody); + } + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + } + return false; + } + + /** + * Send mail using the $Sendmail program. + * @param string $header The message headers + * @param string $body The message body + * @see PHPMailer::$Sendmail + * @throws phpmailerException + * @access protected + * @return boolean + */ + protected function sendmailSend($header, $body) + { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (!empty($this->Sender) and self::isShellSafe($this->Sender)) { + if ($this->Mailer == 'qmail') { + $sendmailFmt = '%s -f%s'; + } else { + $sendmailFmt = '%s -oi -f%s -t'; + } + } else { + if ($this->Mailer == 'qmail') { + $sendmailFmt = '%s'; + } else { + $sendmailFmt = '%s -oi -t'; + } + } + + // TODO: If possible, this should be changed to escapeshellarg. Needs thorough testing. + $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender); + + if ($this->SingleTo) { + foreach ($this->SingleToArray as $toAddr) { + if (!@$mail = popen($sendmail, 'w')) { + throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fputs($mail, 'To: ' . $toAddr . "\n"); + fputs($mail, $header); + fputs($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + array($toAddr), + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); + if ($result != 0) { + throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + } else { + if (!@$mail = popen($sendmail, 'w')) { + throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + fputs($mail, $header); + fputs($mail, $body); + $result = pclose($mail); + $this->doCallback( + ($result == 0), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); + if ($result != 0) { + throw new phpmailerException($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + } + } + return true; + } + + /** + * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters. + * + * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows. + * @param string $string The string to be validated + * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report + * @access protected + * @return boolean + */ + protected static function isShellSafe($string) + { + // Future-proof + if (escapeshellcmd($string) !== $string + or !in_array(escapeshellarg($string), array("'$string'", "\"$string\"")) + ) { + return false; + } + + $length = strlen($string); + + for ($i = 0; $i < $length; $i++) { + $c = $string[$i]; + + // All other characters have a special meaning in at least one common shell, including = and +. + // Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here. + // Note that this does permit non-Latin alphanumeric characters based on the current locale. + if (!ctype_alnum($c) && strpos('@_-.', $c) === false) { + return false; + } + } + + return true; + } + + /** + * Send mail using the PHP mail() function. + * @param string $header The message headers + * @param string $body The message body + * @link http://www.php.net/manual/en/book.mail.php + * @throws phpmailerException + * @access protected + * @return boolean + */ + protected function mailSend($header, $body) + { + $toArr = array(); + foreach ($this->to as $toaddr) { + $toArr[] = $this->addrFormat($toaddr); + } + $to = implode(', ', $toArr); + + $params = null; + //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { + // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped. + if (self::isShellSafe($this->Sender)) { + $params = sprintf('-f%s', $this->Sender); + } + } + if (!empty($this->Sender) and !ini_get('safe_mode') and $this->validateAddress($this->Sender)) { + $old_from = ini_get('sendmail_from'); + ini_set('sendmail_from', $this->Sender); + } + $result = false; + if ($this->SingleTo and count($toArr) > 1) { + foreach ($toArr as $toAddr) { + $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); + $this->doCallback($result, array($toAddr), $this->cc, $this->bcc, $this->Subject, $body, $this->From); + } + } else { + $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); + $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From); + } + if (isset($old_from)) { + ini_set('sendmail_from', $old_from); + } + if (!$result) { + throw new phpmailerException($this->lang('instantiate'), self::STOP_CRITICAL); + } + return true; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation + * @return SMTP + */ + public function getSMTPInstance() + { + if (!is_object($this->smtp)) { + $this->smtp = new SMTP; + } + return $this->smtp; + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * Uses the PHPMailerSMTP class by default. + * @see PHPMailer::getSMTPInstance() to use a different class. + * @param string $header The message headers + * @param string $body The message body + * @throws phpmailerException + * @uses SMTP + * @access protected + * @return boolean + */ + protected function smtpSend($header, $body) + { + $bad_rcpt = array(); + if (!$this->smtpConnect($this->SMTPOptions)) { + throw new phpmailerException($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + } + if (!empty($this->Sender) and $this->validateAddress($this->Sender)) { + $smtp_from = $this->Sender; + } else { + $smtp_from = $this->From; + } + if (!$this->smtp->mail($smtp_from)) { + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + throw new phpmailerException($this->ErrorInfo, self::STOP_CRITICAL); + } + + // Attempt to send to all recipients + foreach (array($this->to, $this->cc, $this->bcc) as $togroup) { + foreach ($togroup as $to) { + if (!$this->smtp->recipient($to[0])) { + $error = $this->smtp->getError(); + $bad_rcpt[] = array('to' => $to[0], 'error' => $error['detail']); + $isSent = false; + } else { + $isSent = true; + } + $this->doCallback($isSent, array($to[0]), array(), array(), $this->Subject, $body, $this->From); + } + } + + // Only send the DATA command if we have viable recipients + if ((count($this->all_recipients) > count($bad_rcpt)) and !$this->smtp->data($header . $body)) { + throw new phpmailerException($this->lang('data_not_accepted'), self::STOP_CRITICAL); + } + if ($this->SMTPKeepAlive) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + //Create error message for any bad addresses + if (count($bad_rcpt) > 0) { + $errstr = ''; + foreach ($bad_rcpt as $bad) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new phpmailerException( + $this->lang('recipients_failed') . $errstr, + self::STOP_CONTINUE + ); + } + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * @param array $options An array of options compatible with stream_context_create() + * @uses SMTP + * @access public + * @throws phpmailerException + * @return boolean + */ + public function smtpConnect($options = null) + { + if (is_null($this->smtp)) { + $this->smtp = $this->getSMTPInstance(); + } + + //If no options are provided, use whatever is set in the instance + if (is_null($options)) { + $options = $this->SMTPOptions; + } + + // Already connected? + if ($this->smtp->connected()) { + return true; + } + + $this->smtp->setTimeout($this->Timeout); + $this->smtp->setDebugLevel($this->SMTPDebug); + $this->smtp->setDebugOutput($this->Debugoutput); + $this->smtp->setVerp($this->do_verp); + $hosts = explode(';', $this->Host); + $lastexception = null; + + foreach ($hosts as $hostentry) { + $hostinfo = array(); + if (!preg_match('/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim($hostentry), $hostinfo)) { + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ($this->SMTPSecure == 'tls'); + if ('ssl' == $hostinfo[2] or ('' == $hostinfo[2] and 'ssl' == $this->SMTPSecure)) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ($hostinfo[2] == 'tls') { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined('OPENSSL_ALGO_SHA1'); + if ('tls' === $secure or 'ssl' === $secure) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if (!$sslext) { + throw new phpmailerException($this->lang('extension_missing').'openssl', self::STOP_CRITICAL); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (integer)$hostinfo[4]; + if ($tport > 0 and $tport < 65536) { + $port = $tport; + } + if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) { + try { + if ($this->Helo) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello($hello); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ($this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt('STARTTLS')) { + $tls = true; + } + if ($tls) { + if (!$this->smtp->startTLS()) { + throw new phpmailerException($this->lang('connect_host')); + } + // We must resend EHLO after TLS negotiation + $this->smtp->hello($hello); + } + if ($this->SMTPAuth) { + if (!$this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->Realm, + $this->Workstation + ) + ) { + throw new phpmailerException($this->lang('authenticate')); + } + } + return true; + } catch (phpmailerException $exc) { + $lastexception = $exc; + $this->edebug($exc->getMessage()); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ($this->exceptions and !is_null($lastexception)) { + throw $lastexception; + } + return false; + } + + /** + * Close the active SMTP session if one exists. + * @return void + */ + public function smtpClose() + { + if (is_a($this->smtp, 'SMTP')) { + if ($this->smtp->connected()) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + } + + /** + * Set the language for error messages. + * Returns false if it cannot load the language file. + * The default language is English. + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * @return boolean + * @access public + */ + public function setLanguage($langcode = 'en', $lang_path = '') + { + // Backwards compatibility for renamed language codes + $renamed_langcodes = array( + 'br' => 'pt_br', + 'cz' => 'cs', + 'dk' => 'da', + 'no' => 'nb', + 'se' => 'sv', + ); + + if (isset($renamed_langcodes[$langcode])) { + $langcode = $renamed_langcodes[$langcode]; + } + + // Define full set of translatable strings in English + $PHPMAILER_LANG = array( + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ' + ); + if (empty($lang_path)) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname(__FILE__). DIRECTORY_SEPARATOR . 'language'. DIRECTORY_SEPARATOR; + } + //Validate $langcode + if (!preg_match('/^[a-z]{2}(?:_[a-zA-Z]{2})?$/', $langcode)) { + $langcode = 'en'; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ($langcode != 'en') { + // Make sure language file path is readable + if (!is_readable($lang_file)) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } + } + $this->language = $PHPMAILER_LANG; + return (boolean)$foundlang; // Returns false if language not found + } + + /** + * Get the array of strings for the current language. + * @return array + */ + public function getTranslations() + { + return $this->language; + } + + /** + * Create recipient headers. + * @access public + * @param string $type + * @param array $addr An array of recipient, + * where each recipient is a 2-element indexed array with element 0 containing an address + * and element 1 containing a name, like: + * array(array('joe@example.com', 'Joe User'), array('zoe@example.com', 'Zoe User')) + * @return string + */ + public function addrAppend($type, $addr) + { + $addresses = array(); + foreach ($addr as $address) { + $addresses[] = $this->addrFormat($address); + } + return $type . ': ' . implode(', ', $addresses) . $this->LE; + } + + /** + * Format an address for use in a message header. + * @access public + * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name + * like array('joe@example.com', 'Joe User') + * @return string + */ + public function addrFormat($addr) + { + if (empty($addr[1])) { // No name provided + return $this->secureHeader($addr[0]); + } else { + return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') . ' <' . $this->secureHeader( + $addr[0] + ) . '>'; + } + } + + /** + * Word-wrap message. + * For use with mailers that do not automatically perform wrapping + * and for quoted-printable encoded messages. + * Original written by philippe. + * @param string $message The message to wrap + * @param integer $length The line length to wrap to + * @param boolean $qp_mode Whether to run in Quoted-Printable mode + * @access public + * @return string + */ + public function wrapText($message, $length, $qp_mode = false) + { + if ($qp_mode) { + $soft_break = sprintf(' =%s', $this->LE); + } else { + $soft_break = $this->LE; + } + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = (strtolower($this->CharSet) == 'utf-8'); + $lelen = strlen($this->LE); + $crlflen = strlen(self::CRLF); + + $message = $this->fixEOL($message); + //Remove a trailing line break + if (substr($message, -$lelen) == $this->LE) { + $message = substr($message, 0, -$lelen); + } + + //Split message into lines + $lines = explode($this->LE, $message); + //Message will be rebuilt in here + $message = ''; + foreach ($lines as $line) { + $words = explode(' ', $line); + $buf = ''; + $firstword = true; + foreach ($words as $word) { + if ($qp_mode and (strlen($word) > $length)) { + $space_left = $length - strlen($buf) - $crlflen; + if (!$firstword) { + if ($space_left > 20) { + $len = $space_left; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == '=') { + $len--; + } elseif (substr($word, $len - 2, 1) == '=') { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + $buf .= ' ' . $part; + $message .= $buf . sprintf('=%s', self::CRLF); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while (strlen($word) > 0) { + if ($length <= 0) { + break; + } + $len = $length; + if ($is_utf8) { + $len = $this->utf8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == '=') { + $len--; + } elseif (substr($word, $len - 2, 1) == '=') { + $len -= 2; + } + $part = substr($word, 0, $len); + $word = substr($word, $len); + + if (strlen($word) > 0) { + $message .= $part . sprintf('=%s', self::CRLF); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + if (!$firstword) { + $buf .= ' '; + } + $buf .= $word; + + if (strlen($buf) > $length and $buf_o != '') { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + $firstword = false; + } + $message .= $buf . self::CRLF; + } + + return $message; + } + + /** + * Find the last character boundary prior to $maxLength in a utf-8 + * quoted-printable encoded string. + * Original written by Colin Brown. + * @access public + * @param string $encodedText utf-8 QP text + * @param integer $maxLength Find the last character boundary prior to this length + * @return integer + */ + public function utf8CharBoundary($encodedText, $maxLength) + { + $foundSplitPos = false; + $lookBack = 3; + while (!$foundSplitPos) { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, '='); + if (false !== $encodedCharPos) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) { + // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + if ($encodedCharPos > 0) { + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + } + $foundSplitPos = true; + } elseif ($dec >= 192) { + // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + $foundSplitPos = true; + } elseif ($dec < 192) { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + return $maxLength; + } + + /** + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. + * @access public + * @return void + */ + public function setWordWrap() + { + if ($this->WordWrap < 1) { + return; + } + + switch ($this->message_type) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap); + break; + default: + $this->Body = $this->wrapText($this->Body, $this->WordWrap); + break; + } + } + + /** + * Assemble message headers. + * @access public + * @return string The assembled headers + */ + public function createHeader() + { + $result = ''; + + if ($this->MessageDate == '') { + $this->MessageDate = self::rfcDate(); + } + $result .= $this->headerLine('Date', $this->MessageDate); + + // To be created automatically by mail() + if ($this->SingleTo) { + if ($this->Mailer != 'mail') { + foreach ($this->to as $toaddr) { + $this->SingleToArray[] = $this->addrFormat($toaddr); + } + } + } else { + if (count($this->to) > 0) { + if ($this->Mailer != 'mail') { + $result .= $this->addrAppend('To', $this->to); + } + } elseif (count($this->cc) == 0) { + $result .= $this->headerLine('To', 'undisclosed-recipients:;'); + } + } + + $result .= $this->addrAppend('From', array(array(trim($this->From), $this->FromName))); + + // sendmail and mail() extract Cc from the header before sending + if (count($this->cc) > 0) { + $result .= $this->addrAppend('Cc', $this->cc); + } + + // sendmail and mail() extract Bcc from the header before sending + if (( + $this->Mailer == 'sendmail' or $this->Mailer == 'qmail' or $this->Mailer == 'mail' + ) + and count($this->bcc) > 0 + ) { + $result .= $this->addrAppend('Bcc', $this->bcc); + } + + if (count($this->ReplyTo) > 0) { + $result .= $this->addrAppend('Reply-To', $this->ReplyTo); + } + + // mail() sets the subject itself + if ($this->Mailer != 'mail') { + $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject))); + } + + // Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4 + // https://tools.ietf.org/html/rfc5322#section-3.6.4 + if ('' != $this->MessageID and preg_match('/^<.*@.*>$/', $this->MessageID)) { + $this->lastMessageID = $this->MessageID; + } else { + $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname()); + } + $result .= $this->headerLine('Message-ID', $this->lastMessageID); + if (!is_null($this->Priority)) { + $result .= $this->headerLine('X-Priority', $this->Priority); + } + if ($this->XMailer == '') { + $result .= $this->headerLine( + 'X-Mailer', + 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer)' + ); + } else { + $myXmailer = trim($this->XMailer); + if ($myXmailer) { + $result .= $this->headerLine('X-Mailer', $myXmailer); + } + } + + if ($this->ConfirmReadingTo != '') { + $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>'); + } + + // Add custom headers + foreach ($this->CustomHeader as $header) { + $result .= $this->headerLine( + trim($header[0]), + $this->encodeHeader(trim($header[1])) + ); + } + if (!$this->sign_key_file) { + $result .= $this->headerLine('MIME-Version', '1.0'); + $result .= $this->getMailMIME(); + } + + return $result; + } + + /** + * Get the message MIME type headers. + * @access public + * @return string + */ + public function getMailMIME() + { + $result = ''; + $ismultipart = true; + switch ($this->message_type) { + case 'inline': + $result .= $this->headerLine('Content-Type', 'multipart/related;'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->headerLine('Content-Type', 'multipart/mixed;'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + case 'alt': + case 'alt_inline': + $result .= $this->headerLine('Content-Type', 'multipart/alternative;'); + $result .= $this->textLine("\tboundary=\"" . $this->boundary[1] . '"'); + break; + default: + // Catches case 'plain': and case '': + $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet); + $ismultipart = false; + break; + } + // RFC1341 part 5 says 7bit is assumed if not specified + if ($this->Encoding != '7bit') { + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ($ismultipart) { + if ($this->Encoding == '8bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', '8bit'); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding); + } + } + + if ($this->Mailer != 'mail') { + $result .= $this->LE; + } + + return $result; + } + + /** + * Returns the whole MIME message. + * Includes complete headers and body. + * Only valid post preSend(). + * @see PHPMailer::preSend() + * @access public + * @return string + */ + public function getSentMIMEMessage() + { + return rtrim($this->MIMEHeader . $this->mailHeader, "\n\r") . self::CRLF . self::CRLF . $this->MIMEBody; + } + + /** + * Create unique ID + * @return string + */ + protected function generateId() { + return md5(uniqid(time())); + } + + /** + * Assemble the message body. + * Returns an empty string on failure. + * @access public + * @throws phpmailerException + * @return string The assembled message body + */ + public function createBody() + { + $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = $this->generateId(); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if ($this->sign_key_file) { + $body .= $this->getMailMIME() . $this->LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) { + $bodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the body part only + if ('base64' != $this->Encoding and self::hasLineLongerThanMax($this->Body)) { + $bodyEncoding = 'quoted-printable'; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) { + $altBodyEncoding = '7bit'; + //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding for the alt body part only + if ('base64' != $altBodyEncoding and self::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = 'quoted-printable'; + } + //Use this as a preamble in all multipart message types + $mimepre = "This is a multi-part message in MIME format." . $this->LE . $this->LE; + switch ($this->message_type) { + case 'inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll('inline', $this->boundary[1]); + break; + case 'attach': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', 'multipart/related;'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= $this->LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + if (!empty($this->Ical)) { + $body .= $this->getBoundary($this->boundary[1], '', 'text/calendar; method=REQUEST', ''); + $body .= $this->encodeString($this->Ical, $this->Encoding); + $body .= $this->LE . $this->LE; + } + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_inline': + $body .= $mimepre; + $body .= $this->getBoundary($this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', 'multipart/related;'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll('inline', $this->boundary[2]); + $body .= $this->LE; + $body .= $this->endBoundary($this->boundary[1]); + break; + case 'alt_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->endBoundary($this->boundary[2]); + $body .= $this->LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + case 'alt_inline_attach': + $body .= $mimepre; + $body .= $this->textLine('--' . $this->boundary[1]); + $body .= $this->headerLine('Content-Type', 'multipart/alternative;'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[2] . '"'); + $body .= $this->LE; + $body .= $this->getBoundary($this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding); + $body .= $this->encodeString($this->AltBody, $altBodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->textLine('--' . $this->boundary[2]); + $body .= $this->headerLine('Content-Type', 'multipart/related;'); + $body .= $this->textLine("\tboundary=\"" . $this->boundary[3] . '"'); + $body .= $this->LE; + $body .= $this->getBoundary($this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding); + $body .= $this->encodeString($this->Body, $bodyEncoding); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll('inline', $this->boundary[3]); + $body .= $this->LE; + $body .= $this->endBoundary($this->boundary[2]); + $body .= $this->LE; + $body .= $this->attachAll('attachment', $this->boundary[1]); + break; + default: + // Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types + //Reset the `Encoding` property in case we changed it for line length reasons + $this->Encoding = $bodyEncoding; + $body .= $this->encodeString($this->Body, $this->Encoding); + break; + } + + if ($this->isError()) { + $body = ''; + } elseif ($this->sign_key_file) { + try { + if (!defined('PKCS7_TEXT')) { + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); + } + // @TODO would be nice to use php://temp streams here, but need to wrap for PHP < 5.1 + $file = tempnam(sys_get_temp_dir(), 'mail'); + if (false === file_put_contents($file, $body)) { + throw new phpmailerException($this->lang('signing') . ' Could not write temp file'); + } + $signed = tempnam(sys_get_temp_dir(), 'signed'); + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if (empty($this->sign_extracerts_file)) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath($this->sign_cert_file), + array('file://' . realpath($this->sign_key_file), $this->sign_key_pass), + null, + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + if ($sign) { + @unlink($file); + $body = file_get_contents($signed); + @unlink($signed); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode("\n\n", $body, 2); + $this->MIMEHeader .= $parts[0] . $this->LE . $this->LE; + $body = $parts[1]; + } else { + @unlink($file); + @unlink($signed); + throw new phpmailerException($this->lang('signing') . openssl_error_string()); + } + } catch (phpmailerException $exc) { + $body = ''; + if ($this->exceptions) { + throw $exc; + } + } + } + return $body; + } + + /** + * Return the start of a message boundary. + * @access protected + * @param string $boundary + * @param string $charSet + * @param string $contentType + * @param string $encoding + * @return string + */ + protected function getBoundary($boundary, $charSet, $contentType, $encoding) + { + $result = ''; + if ($charSet == '') { + $charSet = $this->CharSet; + } + if ($contentType == '') { + $contentType = $this->ContentType; + } + if ($encoding == '') { + $encoding = $this->Encoding; + } + $result .= $this->textLine('--' . $boundary); + $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet); + $result .= $this->LE; + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding != '7bit') { + $result .= $this->headerLine('Content-Transfer-Encoding', $encoding); + } + $result .= $this->LE; + + return $result; + } + + /** + * Return the end of a message boundary. + * @access protected + * @param string $boundary + * @return string + */ + protected function endBoundary($boundary) + { + return $this->LE . '--' . $boundary . '--' . $this->LE; + } + + /** + * Set the message type. + * PHPMailer only supports some preset message types, not arbitrary MIME structures. + * @access protected + * @return void + */ + protected function setMessageType() + { + $type = array(); + if ($this->alternativeExists()) { + $type[] = 'alt'; + } + if ($this->inlineImageExists()) { + $type[] = 'inline'; + } + if ($this->attachmentExists()) { + $type[] = 'attach'; + } + $this->message_type = implode('_', $type); + if ($this->message_type == '') { + //The 'plain' message_type refers to the message having a single body element, not that it is plain-text + $this->message_type = 'plain'; + } + } + + /** + * Format a header line. + * @access public + * @param string $name + * @param string $value + * @return string + */ + public function headerLine($name, $value) + { + return $name . ': ' . $value . $this->LE; + } + + /** + * Return a formatted mail line. + * @access public + * @param string $value + * @return string + */ + public function textLine($value) + { + return $value . $this->LE; + } + + /** + * Add an attachment from a path on the filesystem. + * Never use a user-supplied path to a file! + * Returns false if the file could not be found or read. + * @param string $path Path to the attachment. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @param string $disposition Disposition to use + * @throws phpmailerException + * @return boolean + */ + public function addAttachment($path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment') + { + try { + if (!@is_file($path)) { + throw new phpmailerException($this->lang('file_access') . $path, self::STOP_CONTINUE); + } + + // If a MIME type is not specified, try to work it out from the file name + if ($type == '') { + $type = self::filenameToType($path); + } + + $filename = basename($path); + if ($name == '') { + $name = $filename; + } + + $this->attachment[] = array( + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => 0 + ); + + } catch (phpmailerException $exc) { + $this->setError($exc->getMessage()); + $this->edebug($exc->getMessage()); + if ($this->exceptions) { + throw $exc; + } + return false; + } + return true; + } + + /** + * Return the array of attachments. + * @return array + */ + public function getAttachments() + { + return $this->attachment; + } + + /** + * Attach all file, string, and binary attachments to the message. + * Returns an empty string on failure. + * @access protected + * @param string $disposition_type + * @param string $boundary + * @return string + */ + protected function attachAll($disposition_type, $boundary) + { + // Return text of body + $mime = array(); + $cidUniq = array(); + $incl = array(); + + // Add all attachments + foreach ($this->attachment as $attachment) { + // Check if it is a valid disposition_filter + if ($attachment[6] == $disposition_type) { + // Check for string attachment + $string = ''; + $path = ''; + $bString = $attachment[5]; + if ($bString) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; + } + + $inclhash = md5(serialize($attachment)); + if (in_array($inclhash, $incl)) { + continue; + } + $incl[] = $inclhash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ($disposition == 'inline' && array_key_exists($cid, $cidUniq)) { + continue; + } + $cidUniq[$cid] = true; + + $mime[] = sprintf('--%s%s', $boundary, $this->LE); + //Only include a filename property if we have one + if (!empty($name)) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader($this->secureHeader($name)), + $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + $this->LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if ($encoding != '7bit') { + $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, $this->LE); + } + + if ($disposition == 'inline') { + $mime[] = sprintf('Content-ID: <%s>%s', $cid, $this->LE); + } + + // If a filename contains any of these chars, it should be quoted, + // but not otherwise: RFC2183 & RFC2045 5.1 + // Fixes a warning in IETF's msglint MIME checker + // Allow for bypassing the Content-Disposition header totally + if (!(empty($disposition))) { + $encoded_name = $this->encodeHeader($this->secureHeader($name)); + if (preg_match('/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename="%s"%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + if (!empty($encoded_name)) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + $this->LE . $this->LE + ); + } + } + } else { + $mime[] = $this->LE; + } + + // Encode as string attachment + if ($bString) { + $mime[] = $this->encodeString($string, $encoding); + if ($this->isError()) { + return ''; + } + $mime[] = $this->LE . $this->LE; + } else { + $mime[] = $this->encodeFile($path, $encoding); + if ($this->isError()) { + return ''; + } + $mime[] = $this->LE . $this->LE; + } + } + } + + $mime[] = sprintf('--%s--%s', $boundary, $this->LE); + + return implode('', $mime); + } + + /** + * Encode a file attachment in requested format. + * Returns an empty string on failure. + * @param string $path The full path to the file + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * @throws phpmailerException + * @access protected + * @return string + */ + protected function encodeFile($path, $encoding = 'base64') + { + try { + if (!is_readable($path)) { + throw new phpmailerException($this->lang('file_open') . $path, self::STOP_CONTINUE); + } + $magic_quotes = get_magic_quotes_runtime(); + if ($magic_quotes) { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + set_magic_quotes_runtime(false); + } else { + //Doesn't exist in PHP 5.4, but we don't need to check because + //get_magic_quotes_runtime always returns false in 5.4+ + //so it will never get here + ini_set('magic_quotes_runtime', false); + } + } + $file_buffer = file_get_contents($path); + $file_buffer = $this->encodeString($file_buffer, $encoding); + if ($magic_quotes) { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + set_magic_quotes_runtime($magic_quotes); + } else { + ini_set('magic_quotes_runtime', $magic_quotes); + } + } + return $file_buffer; + } catch (Exception $exc) { + $this->setError($exc->getMessage()); + return ''; + } + } + + /** + * Encode a string in requested format. + * Returns an empty string on failure. + * @param string $str The text to encode + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * @access public + * @return string + */ + public function encodeString($str, $encoding = 'base64') + { + $encoded = ''; + switch (strtolower($encoding)) { + case 'base64': + $encoded = chunk_split(base64_encode($str), 76, $this->LE); + break; + case '7bit': + case '8bit': + $encoded = $this->fixEOL($str); + // Make sure it ends with a line break + if (substr($encoded, -(strlen($this->LE))) != $this->LE) { + $encoded .= $this->LE; + } + break; + case 'binary': + $encoded = $str; + break; + case 'quoted-printable': + $encoded = $this->encodeQP($str); + break; + default: + $this->setError($this->lang('encoding') . $encoding); + break; + } + return $encoded; + } + + /** + * Encode a header string optimally. + * Picks shortest of Q, B, quoted-printable or none. + * @access public + * @param string $str + * @param string $position + * @return string + */ + public function encodeHeader($str, $position = 'text') + { + $matchcount = 0; + switch (strtolower($position)) { + case 'phrase': + if (!preg_match('/[\200-\377]/', $str)) { + // Can't use addslashes as we don't know the value of magic_quotes_sybase + $encoded = addcslashes($str, "\0..\37\177\\\""); + if (($str == $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) { + return ($encoded); + } else { + return ("\"$encoded\""); + } + } + $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches); + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = preg_match_all('/[()"]/', $str, $matches); + // Intentional fall-through + case 'text': + default: + $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches); + break; + } + + //There are no chars that need encoding + if ($matchcount == 0) { + return ($str); + } + + $maxlen = 75 - 7 - strlen($this->CharSet); + // Try to select the encoding which should produce the shortest output + if ($matchcount > strlen($str) / 3) { + // More than a third of the content will need encoding, so B encoding will be most efficient + $encoding = 'B'; + if (function_exists('mb_strlen') && $this->hasMultiBytes($str)) { + // Use a custom function which correctly encodes and wraps long + // multibyte strings without breaking lines within a character + $encoded = $this->base64EncodeWrapMB($str, "\n"); + } else { + $encoded = base64_encode($str); + $maxlen -= $maxlen % 4; + $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + } + } else { + $encoding = 'Q'; + $encoded = $this->encodeQ($str, $position); + $encoded = $this->wrapText($encoded, $maxlen, true); + $encoded = str_replace('=' . self::CRLF, "\n", trim($encoded)); + } + + $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); + $encoded = trim(str_replace("\n", $this->LE, $encoded)); + + return $encoded; + } + + /** + * Check if a string contains multi-byte characters. + * @access public + * @param string $str multi-byte text to wrap encode + * @return boolean + */ + public function hasMultiBytes($str) + { + if (function_exists('mb_strlen')) { + return (strlen($str) > mb_strlen($str, $this->CharSet)); + } else { // Assume no multibytes (we can't handle without mbstring functions anyway) + return false; + } + } + + /** + * Does a string contain any 8-bit chars (in any charset)? + * @param string $text + * @return boolean + */ + public function has8bitChars($text) + { + return (boolean)preg_match('/[\x80-\xFF]/', $text); + } + + /** + * Encode and wrap long multibyte strings for mail headers + * without breaking lines within a character. + * Adapted from a function by paravoid + * @link http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 + * @access public + * @param string $str multi-byte text to wrap encode + * @param string $linebreak string to use as linefeed/end-of-line + * @return string + */ + public function base64EncodeWrapMB($str, $linebreak = null) + { + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if ($linebreak === null) { + $linebreak = $this->LE; + } + + $mb_length = mb_strlen($str, $this->CharSet); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen($start) - strlen($end); + // Average multi-byte ratio + $ratio = $mb_length / strlen($str); + // Base64 has a 4:3 ratio + $avgLength = floor($length * $ratio * .75); + + for ($i = 0; $i < $mb_length; $i += $offset) { + $lookBack = 0; + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr($str, $i, $offset, $this->CharSet); + $chunk = base64_encode($chunk); + $lookBack++; + } while (strlen($chunk) > $length); + $encoded .= $chunk . $linebreak; + } + + // Chomp the last linefeed + $encoded = substr($encoded, 0, -strlen($linebreak)); + return $encoded; + } + + /** + * Encode a string in quoted-printable format. + * According to RFC2045 section 6.7. + * @access public + * @param string $string The text to encode + * @param integer $line_max Number of chars allowed on a line before wrapping + * @return string + * @link http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 Adapted from this comment + */ + public function encodeQP($string, $line_max = 76) + { + // Use native function if it's available (>= PHP5.3) + if (function_exists('quoted_printable_encode')) { + return quoted_printable_encode($string); + } + // Fall back to a pure PHP implementation + $string = str_replace( + array('%20', '%0D%0A.', '%0D%0A', '%'), + array(' ', "\r\n=2E", "\r\n", '='), + rawurlencode($string) + ); + return preg_replace('/[^\r\n]{' . ($line_max - 3) . '}[^=\r\n]{2}/', "$0=\r\n", $string); + } + + /** + * Backward compatibility wrapper for an old QP encoding function that was removed. + * @see PHPMailer::encodeQP() + * @access public + * @param string $string + * @param integer $line_max + * @param boolean $space_conv + * @return string + * @deprecated Use encodeQP instead. + */ + public function encodeQPphp( + $string, + $line_max = 76, + /** @noinspection PhpUnusedParameterInspection */ $space_conv = false + ) { + return $this->encodeQP($string, $line_max); + } + + /** + * Encode a string using Q encoding. + * @link http://tools.ietf.org/html/rfc2047 + * @param string $str the text to encode + * @param string $position Where the text is going to be used, see the RFC for what that means + * @access public + * @return string + */ + public function encodeQ($str, $position = 'text') + { + // There should not be any EOL in the string + $pattern = ''; + $encoded = str_replace(array("\r", "\n"), '', $str); + switch (strtolower($position)) { + case 'phrase': + // RFC 2047 section 5.3 + $pattern = '^A-Za-z0-9!*+\/ -'; + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + // RFC 2047 section 5.2 + $pattern = '\(\)"'; + // intentional fall-through + // for this reason we build the $pattern without including delimiters and [] + case 'text': + default: + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters + $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; + break; + } + $matches = array(); + if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) { + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search('=', $matches[0]); + if (false !== $eqkey) { + unset($matches[0][$eqkey]); + array_unshift($matches[0], '='); + } + foreach (array_unique($matches[0]) as $char) { + $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded); + } + } + // Replace every spaces to _ (more readable than =20) + return str_replace(' ', '_', $encoded); + } + + /** + * Add a string or binary attachment (non-filesystem). + * This method can be used to attach ascii or binary data, + * such as a BLOB record from a database. + * @param string $string String attachment data. + * @param string $filename Name of the attachment. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @param string $disposition Disposition to use + * @return void + */ + public function addStringAttachment( + $string, + $filename, + $encoding = 'base64', + $type = '', + $disposition = 'attachment' + ) { + // If a MIME type is not specified, try to work it out from the file name + if ($type == '') { + $type = self::filenameToType($filename); + } + // Append to $attachment array + $this->attachment[] = array( + 0 => $string, + 1 => $filename, + 2 => basename($filename), + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => 0 + ); + } + + /** + * Add an embedded (inline) attachment from a file. + * This can include images, sounds, and just about any other document type. + * These differ from 'regular' attachments in that they are intended to be + * displayed inline with the message, not just attached for download. + * This is used in HTML messages that embed the images + * the HTML refers to using the $cid value. + * Never use a user-supplied path to a file! + * @param string $path Path to the attachment. + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File MIME type. + * @param string $disposition Disposition to use + * @return boolean True on successfully adding an attachment + */ + public function addEmbeddedImage($path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline') + { + if (!@is_file($path)) { + $this->setError($this->lang('file_access') . $path); + return false; + } + + // If a MIME type is not specified, try to work it out from the file name + if ($type == '') { + $type = self::filenameToType($path); + } + + $filename = basename($path); + if ($name == '') { + $name = $filename; + } + + // Append to $attachment array + $this->attachment[] = array( + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $cid + ); + return true; + } + + /** + * Add an embedded stringified attachment. + * This can include images, sounds, and just about any other document type. + * Be sure to set the $type to an image type for images: + * JPEG images use 'image/jpeg', GIF uses 'image/gif', PNG uses 'image/png'. + * @param string $string The attachment binary data. + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML. + * @param string $name + * @param string $encoding File encoding (see $Encoding). + * @param string $type MIME type. + * @param string $disposition Disposition to use + * @return boolean True on successfully adding an attachment + */ + public function addStringEmbeddedImage( + $string, + $cid, + $name = '', + $encoding = 'base64', + $type = '', + $disposition = 'inline' + ) { + // If a MIME type is not specified, try to work it out from the name + if ($type == '' and !empty($name)) { + $type = self::filenameToType($name); + } + + // Append to $attachment array + $this->attachment[] = array( + 0 => $string, + 1 => $name, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => $cid + ); + return true; + } + + /** + * Check if an inline attachment is present. + * @access public + * @return boolean + */ + public function inlineImageExists() + { + foreach ($this->attachment as $attachment) { + if ($attachment[6] == 'inline') { + return true; + } + } + return false; + } + + /** + * Check if an attachment (non-inline) is present. + * @return boolean + */ + public function attachmentExists() + { + foreach ($this->attachment as $attachment) { + if ($attachment[6] == 'attachment') { + return true; + } + } + return false; + } + + /** + * Check if this message has an alternative body set. + * @return boolean + */ + public function alternativeExists() + { + return !empty($this->AltBody); + } + + /** + * Clear queued addresses of given kind. + * @access protected + * @param string $kind 'to', 'cc', or 'bcc' + * @return void + */ + public function clearQueuedAddresses($kind) + { + $RecipientsQueue = $this->RecipientsQueue; + foreach ($RecipientsQueue as $address => $params) { + if ($params[0] == $kind) { + unset($this->RecipientsQueue[$address]); + } + } + } + + /** + * Clear all To recipients. + * @return void + */ + public function clearAddresses() + { + foreach ($this->to as $to) { + unset($this->all_recipients[strtolower($to[0])]); + } + $this->to = array(); + $this->clearQueuedAddresses('to'); + } + + /** + * Clear all CC recipients. + * @return void + */ + public function clearCCs() + { + foreach ($this->cc as $cc) { + unset($this->all_recipients[strtolower($cc[0])]); + } + $this->cc = array(); + $this->clearQueuedAddresses('cc'); + } + + /** + * Clear all BCC recipients. + * @return void + */ + public function clearBCCs() + { + foreach ($this->bcc as $bcc) { + unset($this->all_recipients[strtolower($bcc[0])]); + } + $this->bcc = array(); + $this->clearQueuedAddresses('bcc'); + } + + /** + * Clear all ReplyTo recipients. + * @return void + */ + public function clearReplyTos() + { + $this->ReplyTo = array(); + $this->ReplyToQueue = array(); + } + + /** + * Clear all recipient types. + * @return void + */ + public function clearAllRecipients() + { + $this->to = array(); + $this->cc = array(); + $this->bcc = array(); + $this->all_recipients = array(); + $this->RecipientsQueue = array(); + } + + /** + * Clear all filesystem, string, and binary attachments. + * @return void + */ + public function clearAttachments() + { + $this->attachment = array(); + } + + /** + * Clear all custom headers. + * @return void + */ + public function clearCustomHeaders() + { + $this->CustomHeader = array(); + } + + /** + * Add an error message to the error container. + * @access protected + * @param string $msg + * @return void + */ + protected function setError($msg) + { + $this->error_count++; + if ($this->Mailer == 'smtp' and !is_null($this->smtp)) { + $lasterror = $this->smtp->getError(); + if (!empty($lasterror['error'])) { + $msg .= $this->lang('smtp_error') . $lasterror['error']; + if (!empty($lasterror['detail'])) { + $msg .= ' Detail: '. $lasterror['detail']; + } + if (!empty($lasterror['smtp_code'])) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if (!empty($lasterror['smtp_code_ex'])) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } + } + } + $this->ErrorInfo = $msg; + } + + /** + * Return an RFC 822 formatted date. + * @access public + * @return string + * @static + */ + public static function rfcDate() + { + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini + date_default_timezone_set(@date_default_timezone_get()); + return date('D, j M Y H:i:s O'); + } + + /** + * Get the server hostname. + * Returns 'localhost.localdomain' if unknown. + * @access protected + * @return string + */ + protected function serverHostname() + { + $result = 'localhost.localdomain'; + if (!empty($this->Hostname)) { + $result = $this->Hostname; + } elseif (isset($_SERVER) and array_key_exists('SERVER_NAME', $_SERVER) and !empty($_SERVER['SERVER_NAME'])) { + $result = $_SERVER['SERVER_NAME']; + } elseif (function_exists('gethostname') && gethostname() !== false) { + $result = gethostname(); + } elseif (php_uname('n') !== false) { + $result = php_uname('n'); + } + return $result; + } + + /** + * Get an error message in the current language. + * @access protected + * @param string $key + * @return string + */ + protected function lang($key) + { + if (count($this->language) < 1) { + $this->setLanguage('en'); // set the default language + } + + if (array_key_exists($key, $this->language)) { + if ($key == 'smtp_connect_failed') { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } + return $this->language[$key]; + } else { + //Return the key as a fallback + return $key; + } + } + + /** + * Check if an error occurred. + * @access public + * @return boolean True if an error did occur. + */ + public function isError() + { + return ($this->error_count > 0); + } + + /** + * Ensure consistent line endings in a string. + * Changes every end of line from CRLF, CR or LF to $this->LE. + * @access public + * @param string $str String to fixEOL + * @return string + */ + public function fixEOL($str) + { + // Normalise to \n + $nstr = str_replace(array("\r\n", "\r"), "\n", $str); + // Now convert LE as needed + if ($this->LE !== "\n") { + $nstr = str_replace("\n", $this->LE, $nstr); + } + return $nstr; + } + + /** + * Add a custom header. + * $name value can be overloaded to contain + * both header name and value (name:value) + * @access public + * @param string $name Custom header name + * @param string $value Header value + * @return void + */ + public function addCustomHeader($name, $value = null) + { + if ($value === null) { + // Value passed in as name:value + $this->CustomHeader[] = explode(':', $name, 2); + } else { + $this->CustomHeader[] = array($name, $value); + } + } + + /** + * Returns all custom headers. + * @return array + */ + public function getCustomHeaders() + { + return $this->CustomHeader; + } + + /** + * Create a message body from an HTML string. + * Automatically inlines images and creates a plain-text version by converting the HTML, + * overwriting any existing values in Body and AltBody. + * Do not source $message content from user input! + * $basedir is prepended when handling relative URLs, e.g. and must not be empty + * will look for an image file in $basedir/images/a.png and convert it to inline. + * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email) + * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly. + * @access public + * @param string $message HTML message string + * @param string $basedir Absolute path to a base directory to prepend to relative paths to images + * @param boolean|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see PHPMailer::html2text() + * @return string $message The transformed message Body + */ + public function msgHTML($message, $basedir = '', $advanced = false) + { + preg_match_all('/(src|background)=["\'](.*)["\']/Ui', $message, $images); + if (array_key_exists(2, $images)) { + if (strlen($basedir) > 1 && substr($basedir, -1) != '/') { + // Ensure $basedir has a trailing / + $basedir .= '/'; + } + foreach ($images[2] as $imgindex => $url) { + // Convert data URIs into embedded images + if (preg_match('#^data:(image[^;,]*)(;base64)?,#', $url, $match)) { + $data = substr($url, strpos($url, ',')); + if ($match[2]) { + $data = base64_decode($data); + } else { + $data = rawurldecode($data); + } + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 + if ($this->addStringEmbeddedImage($data, $cid, 'embed' . $imgindex, 'base64', $match[1])) { + $message = str_replace( + $images[0][$imgindex], + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + continue; + } + if ( + // Only process relative URLs if a basedir is provided (i.e. no absolute local paths) + !empty($basedir) + // Ignore URLs containing parent dir traversal (..) + && (strpos($url, '..') === false) + // Do not change urls that are already inline images + && substr($url, 0, 4) !== 'cid:' + // Do not change absolute URLs, including anonymous protocol + && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url) + ) { + $filename = basename($url); + $directory = dirname($url); + if ($directory == '.') { + $directory = ''; + } + $cid = md5($url) . '@phpmailer.0'; // RFC2392 S 2 + if (strlen($directory) > 1 && substr($directory, -1) != '/') { + $directory .= '/'; + } + if ($this->addEmbeddedImage( + $basedir . $directory . $filename, + $cid, + $filename, + 'base64', + self::_mime_types((string)self::mb_pathinfo($filename, PATHINFO_EXTENSION)) + ) + ) { + $message = preg_replace( + '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui', + $images[1][$imgindex] . '="cid:' . $cid . '"', + $message + ); + } + } + } + } + $this->isHTML(true); + // Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better + $this->Body = $this->normalizeBreaks($message); + $this->AltBody = $this->normalizeBreaks($this->html2text($message, $advanced)); + if (!$this->alternativeExists()) { + $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . + self::CRLF . self::CRLF; + } + return $this->Body; + } + + /** + * Convert an HTML string into plain text. + * This is used by msgHTML(). + * Note - older versions of this function used a bundled advanced converter + * which was been removed for license reasons in #232. + * Example usage: + * + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * + * @param string $html The HTML text to convert + * @param boolean|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion. + * @return string + */ + public function html2text($html, $advanced = false) + { + if (is_callable($advanced)) { + return call_user_func($advanced, $html); + } + return html_entity_decode( + trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))), + ENT_QUOTES, + $this->CharSet + ); + } + + /** + * Get the MIME type for a file extension. + * @param string $ext File extension + * @access public + * @return string MIME type of file. + * @static + */ + public static function _mime_types($ext = '') + { + $mimes = array( + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'class' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie' + ); + if (array_key_exists(strtolower($ext), $mimes)) { + return $mimes[strtolower($ext)]; + } + return 'application/octet-stream'; + } + + /** + * Map a file name to a MIME type. + * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. + * @param string $filename A file name or full path, does not need to exist as a file + * @return string + * @static + */ + public static function filenameToType($filename) + { + // In case the path is a URL, strip any query string before getting extension + $qpos = strpos($filename, '?'); + if (false !== $qpos) { + $filename = substr($filename, 0, $qpos); + } + $pathinfo = self::mb_pathinfo($filename); + return self::_mime_types($pathinfo['extension']); + } + + /** + * Multi-byte-safe pathinfo replacement. + * Drop-in replacement for pathinfo(), but multibyte-safe, cross-platform-safe, old-version-safe. + * Works similarly to the one in PHP >= 5.2.0 + * @link http://www.php.net/manual/en/function.pathinfo.php#107461 + * @param string $path A filename or path, does not need to exist as a file + * @param integer|string $options Either a PATHINFO_* constant, + * or a string name to return only the specified piece, allows 'filename' to work on PHP < 5.2 + * @return string|array + * @static + */ + public static function mb_pathinfo($path, $options = null) + { + $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); + $pathinfo = array(); + if (preg_match('%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $pathinfo)) { + if (array_key_exists(1, $pathinfo)) { + $ret['dirname'] = $pathinfo[1]; + } + if (array_key_exists(2, $pathinfo)) { + $ret['basename'] = $pathinfo[2]; + } + if (array_key_exists(5, $pathinfo)) { + $ret['extension'] = $pathinfo[5]; + } + if (array_key_exists(3, $pathinfo)) { + $ret['filename'] = $pathinfo[3]; + } + } + switch ($options) { + case PATHINFO_DIRNAME: + case 'dirname': + return $ret['dirname']; + case PATHINFO_BASENAME: + case 'basename': + return $ret['basename']; + case PATHINFO_EXTENSION: + case 'extension': + return $ret['extension']; + case PATHINFO_FILENAME: + case 'filename': + return $ret['filename']; + default: + return $ret; + } + } + + /** + * Set or reset instance properties. + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. + * Usage Example: + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';` + * @access public + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * @return boolean + * @TODO Should this not be using the __set() magic function? + */ + public function set($name, $value = '') + { + if (property_exists($this, $name)) { + $this->$name = $value; + return true; + } else { + $this->setError($this->lang('variable_set') . $name); + return false; + } + } + + /** + * Strip newlines to prevent header injection. + * @access public + * @param string $str + * @return string + */ + public function secureHeader($str) + { + return trim(str_replace(array("\r", "\n"), '', $str)); + } + + /** + * Normalize line breaks in a string. + * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. + * Defaults to CRLF (for message bodies) and preserves consecutive breaks. + * @param string $text + * @param string $breaktype What kind of line break to use, defaults to CRLF + * @return string + * @access public + * @static + */ + public static function normalizeBreaks($text, $breaktype = "\r\n") + { + return preg_replace('/(\r\n|\r|\n)/ms', $breaktype, $text); + } + + /** + * Set the public and private key files and password for S/MIME signing. + * @access public + * @param string $cert_filename + * @param string $key_filename + * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate + */ + public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '') + { + $this->sign_cert_file = $cert_filename; + $this->sign_key_file = $key_filename; + $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; + } + + /** + * Quoted-Printable-encode a DKIM header. + * @access public + * @param string $txt + * @return string + */ + public function DKIM_QP($txt) + { + $line = ''; + for ($i = 0; $i < strlen($txt); $i++) { + $ord = ord($txt[$i]); + if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord == 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) { + $line .= $txt[$i]; + } else { + $line .= '=' . sprintf('%02X', $ord); + } + } + return $line; + } + + /** + * Generate a DKIM signature. + * @access public + * @param string $signHeader + * @throws phpmailerException + * @return string The DKIM signature value + */ + public function DKIM_Sign($signHeader) + { + if (!defined('PKCS7_TEXT')) { + if ($this->exceptions) { + throw new phpmailerException($this->lang('extension_missing') . 'openssl'); + } + return ''; + } + $privKeyStr = !empty($this->DKIM_private_string) ? $this->DKIM_private_string : file_get_contents($this->DKIM_private); + if ('' != $this->DKIM_passphrase) { + $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase); + } else { + $privKey = openssl_pkey_get_private($privKeyStr); + } + //Workaround for missing digest algorithms in old PHP & OpenSSL versions + //@link http://stackoverflow.com/a/11117338/333340 + if (version_compare(PHP_VERSION, '5.3.0') >= 0 and + in_array('sha256WithRSAEncryption', openssl_get_md_methods(true))) { + if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } + } else { + $pinfo = openssl_pkey_get_details($privKey); + $hash = hash('sha256', $signHeader); + //'Magic' constant for SHA256 from RFC3447 + //@link https://tools.ietf.org/html/rfc3447#page-43 + $t = '3031300d060960864801650304020105000420' . $hash; + $pslen = $pinfo['bits'] / 8 - (strlen($t) / 2 + 3); + $eb = pack('H*', '0001' . str_repeat('FF', $pslen) . '00' . $t); + + if (openssl_private_encrypt($eb, $signature, $privKey, OPENSSL_NO_PADDING)) { + openssl_pkey_free($privKey); + return base64_encode($signature); + } + } + openssl_pkey_free($privKey); + return ''; + } + + /** + * Generate a DKIM canonicalization header. + * @access public + * @param string $signHeader Header + * @return string + */ + public function DKIM_HeaderC($signHeader) + { + $signHeader = preg_replace('/\r\n\s+/', ' ', $signHeader); + $lines = explode("\r\n", $signHeader); + foreach ($lines as $key => $line) { + list($heading, $value) = explode(':', $line, 2); + $heading = strtolower($heading); + $value = preg_replace('/\s{2,}/', ' ', $value); // Compress useless spaces + $lines[$key] = $heading . ':' . trim($value); // Don't forget to remove WSP around the value + } + $signHeader = implode("\r\n", $lines); + return $signHeader; + } + + /** + * Generate a DKIM canonicalization body. + * @access public + * @param string $body Message Body + * @return string + */ + public function DKIM_BodyC($body) + { + if ($body == '') { + return "\r\n"; + } + // stabilize line endings + $body = str_replace("\r\n", "\n", $body); + $body = str_replace("\n", "\r\n", $body); + // END stabilize line endings + while (substr($body, strlen($body) - 4, 4) == "\r\n\r\n") { + $body = substr($body, 0, strlen($body) - 2); + } + return $body; + } + + /** + * Create the DKIM header and body in a new message header. + * @access public + * @param string $headers_line Header lines + * @param string $subject Subject + * @param string $body Body + * @return string + */ + public function DKIM_Add($headers_line, $subject, $body) + { + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms + $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body + $DKIMquery = 'dns/txt'; // Query method + $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) + $subject_header = "Subject: $subject"; + $headers = explode($this->LE, $headers_line); + $from_header = ''; + $to_header = ''; + $date_header = ''; + $current = ''; + foreach ($headers as $header) { + if (strpos($header, 'From:') === 0) { + $from_header = $header; + $current = 'from_header'; + } elseif (strpos($header, 'To:') === 0) { + $to_header = $header; + $current = 'to_header'; + } elseif (strpos($header, 'Date:') === 0) { + $date_header = $header; + $current = 'date_header'; + } else { + if (!empty($$current) && strpos($header, ' =?') === 0) { + $$current .= $header; + } else { + $current = ''; + } + } + } + $from = str_replace('|', '=7C', $this->DKIM_QP($from_header)); + $to = str_replace('|', '=7C', $this->DKIM_QP($to_header)); + $date = str_replace('|', '=7C', $this->DKIM_QP($date_header)); + $subject = str_replace( + '|', + '=7C', + $this->DKIM_QP($subject_header) + ); // Copied header fields (dkim-quoted-printable) + $body = $this->DKIM_BodyC($body); + $DKIMlen = strlen($body); // Length of body + $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body))); // Base64 of packed binary SHA-256 hash of body + if ('' == $this->DKIM_identity) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . + $this->DKIM_selector . + ";\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . + "\th=From:To:Date:Subject;\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . + "\tz=$from\r\n" . + "\t|$to\r\n" . + "\t|$date\r\n" . + "\t|$subject;\r\n" . + "\tbh=" . $DKIMb64 . ";\r\n" . + "\tb="; + $toSign = $this->DKIM_HeaderC( + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $dkimhdrs + ); + $signed = $this->DKIM_Sign($toSign); + return $dkimhdrs . $signed . "\r\n"; + } + + /** + * Detect if a string contains a line longer than the maximum line length allowed. + * @param string $str + * @return boolean + * @static + */ + public static function hasLineLongerThanMax($str) + { + //+2 to include CRLF line break for a 1000 total + return (boolean)preg_match('/^(.{'.(self::MAX_LINE_LENGTH + 2).',})/m', $str); + } + + /** + * Allows for public read access to 'to' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getToAddresses() + { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getCcAddresses() + { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getBccAddresses() + { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getReplyToAddresses() + { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getAllRecipientAddresses() + { + return $this->all_recipients; + } + + /** + * Perform a callback. + * @param boolean $isSent + * @param array $to + * @param array $cc + * @param array $bcc + * @param string $subject + * @param string $body + * @param string $from + */ + protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from) + { + if (!empty($this->action_function) && is_callable($this->action_function)) { + $params = array($isSent, $to, $cc, $bcc, $subject, $body, $from); + call_user_func_array($this->action_function, $params); + } + } +} + +/** + * PHPMailer exception handler + * @package PHPMailer + */ +class phpmailerException extends Exception +{ + /** + * Prettify error message output + * @return string + */ + public function errorMessage() + { + $errorMsg = '' . $this->getMessage() . "
\n"; + return $errorMsg; + } +} diff --git a/app/class/smtp.class.php b/app/class/smtp.class.php new file mode 100644 index 0000000..01cee82 --- /dev/null +++ b/app/class/smtp.class.php @@ -0,0 +1,1251 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailer RFC821 SMTP email transport class. + * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. + * @package PHPMailer + * @author Chris Ryan + * @author Marcus Bointon + */ +class SMTP +{ + /** + * The PHPMailer SMTP version number. + * @var string + */ + const VERSION = '5.2.23'; + + /** + * SMTP line break constant. + * @var string + */ + const CRLF = "\r\n"; + + /** + * The SMTP port to use if one is not specified. + * @var integer + */ + const DEFAULT_SMTP_PORT = 25; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer + */ + const MAX_LINE_LENGTH = 998; + + /** + * Debug level for no output + */ + const DEBUG_OFF = 0; + + /** + * Debug level to show client -> server messages + */ + const DEBUG_CLIENT = 1; + + /** + * Debug level to show client -> server and server -> client messages + */ + const DEBUG_SERVER = 2; + + /** + * Debug level to show connection status, client -> server and server -> client messages + */ + const DEBUG_CONNECTION = 3; + + /** + * Debug level to show all messages + */ + const DEBUG_LOWLEVEL = 4; + + /** + * The PHPMailer SMTP Version number. + * @var string + * @deprecated Use the `VERSION` constant instead + * @see SMTP::VERSION + */ + public $Version = '5.2.23'; + + /** + * SMTP server port number. + * @var integer + * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead + * @see SMTP::DEFAULT_SMTP_PORT + */ + public $SMTP_PORT = 25; + + /** + * SMTP reply line ending. + * @var string + * @deprecated Use the `CRLF` constant instead + * @see SMTP::CRLF + */ + public $CRLF = "\r\n"; + + /** + * Debug output level. + * Options: + * * self::DEBUG_OFF (`0`) No debug output, default + * * self::DEBUG_CLIENT (`1`) Client commands + * * self::DEBUG_SERVER (`2`) Client commands and server responses + * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status + * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages + * @var integer + */ + public $do_debug = self::DEBUG_OFF; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable + */ + public $Debugoutput = 'echo'; + + /** + * Whether to use VERP. + * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Info on VERP + * @var boolean + */ + public $do_verp = false; + + /** + * The timeout value for connection, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * @var integer + */ + public $Timeout = 300; + + /** + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer + */ + public $Timelimit = 300; + + /** + * @var array patterns to extract smtp transaction id from smtp reply + * Only first capture group will be use, use non-capturing group to deal with it + * Extend this class to override this property to fulfil your needs. + */ + protected $smtp_transaction_id_patterns = array( + 'exim' => '/[0-9]{3} OK id=(.*)/', + 'sendmail' => '/[0-9]{3} 2.0.0 (.*) Message/', + 'postfix' => '/[0-9]{3} 2.0.0 Ok: queued as (.*)/' + ); + + /** + * The socket for the server connection. + * @var resource + */ + protected $smtp_conn; + + /** + * Error information, if any, for the last SMTP command. + * @var array + */ + protected $error = array( + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '' + ); + + /** + * The reply the server sent to us for HELO. + * If null, no HELO string has yet been received. + * @var string|null + */ + protected $helo_rply = null; + + /** + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * @var array|null + */ + protected $server_caps = null; + + /** + * The most recent reply received from the server. + * @var string + */ + protected $last_reply = ''; + + /** + * Output debugging info via a user-selected method. + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug + * @param string $str Debug string to output + * @param integer $level The debug level of this message; see DEBUG_* constants + * @return void + */ + protected function edebug($str, $level = 0) + { + if ($level > $this->do_debug) { + return; + } + //Avoid clash with built-in function names + if (!in_array($this->Debugoutput, array('error_log', 'html', 'echo')) and is_callable($this->Debugoutput)) { + call_user_func($this->Debugoutput, $str, $level); + return; + } + switch ($this->Debugoutput) { + case 'error_log': + //Don't output, just log + error_log($str); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace('/[\r\n]+/', '', $str), + ENT_QUOTES, + 'UTF-8' + ) . "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str); + echo gmdate('Y-m-d H:i:s') . "\t" . str_replace( + "\n", + "\n \t ", + trim($str) + ) . "\n"; + } + } + + /** + * Connect to an SMTP server. + * @param string $host SMTP server IP or host name + * @param integer $port The port number to connect to + * @param integer $timeout How long to wait for the connection to open + * @param array $options An array of options for stream_context_create() + * @access public + * @return boolean + */ + public function connect($host, $port = null, $timeout = 30, $options = array()) + { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if (is_null($streamok)) { + $streamok = function_exists('stream_socket_client'); + } + // Clear errors to avoid confusion + $this->setError(''); + // Make sure we are __not__ connected + if ($this->connected()) { + // Already connected, generate error + $this->setError('Already connected to a server'); + return false; + } + if (empty($port)) { + $port = self::DEFAULT_SMTP_PORT; + } + // Connect to the SMTP server + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=" . + var_export($options, true), + self::DEBUG_CONNECTION + ); + $errno = 0; + $errstr = ''; + if ($streamok) { + $socket_context = stream_context_create($options); + set_error_handler(array($this, 'errorHandler')); + $this->smtp_conn = stream_socket_client( + $host . ":" . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + restore_error_handler(); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + "Connection: stream_socket_client not available, falling back to fsockopen", + self::DEBUG_CONNECTION + ); + set_error_handler(array($this, 'errorHandler')); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + restore_error_handler(); + } + // Verify we connected properly + if (!is_resource($this->smtp_conn)) { + $this->setError( + 'Failed to connect to server', + $errno, + $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT + ); + return false; + } + $this->edebug('Connection: opened', self::DEBUG_CONNECTION); + // SMTP server can take longer to respond, give longer timeout for first read + // Windows does not have support for this timeout function + if (substr(PHP_OS, 0, 3) != 'WIN') { + $max = ini_get('max_execution_time'); + // Don't bother if unlimited + if ($max != 0 && $timeout > $max) { + @set_time_limit($timeout); + } + stream_set_timeout($this->smtp_conn, $timeout, 0); + } + // Get any announcement + $announce = $this->get_lines(); + $this->edebug('SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER); + return true; + } + + /** + * Initiate a TLS (encrypted) session. + * @access public + * @return boolean + */ + public function startTLS() + { + if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { + return false; + } + + //Allow the best TLS version(s) we can + $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; + + //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT + //so add them back in manually if we can + if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + } + + // Begin encrypted connection + set_error_handler(array($this, 'errorHandler')); + $crypto_ok = stream_socket_enable_crypto( + $this->smtp_conn, + true, + $crypto_method + ); + restore_error_handler(); + return $crypto_ok; + } + + /** + * Perform SMTP authentication. + * Must be run after hello(). + * @see hello() + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2) + * @param string $realm The auth realm for NTLM + * @param string $workstation The auth workstation for NTLM + * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth) + * @return bool True if successfully authenticated.* @access public + */ + public function authenticate( + $username, + $password, + $authtype = null, + $realm = '', + $workstation = '', + $OAuth = null + ) { + if (!$this->server_caps) { + $this->setError('Authentication is not allowed before HELO/EHLO'); + return false; + } + + if (array_key_exists('EHLO', $this->server_caps)) { + // SMTP extensions are available; try to find a proper authentication method + if (!array_key_exists('AUTH', $this->server_caps)) { + $this->setError('Authentication is not allowed at this stage'); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + return false; + } + + self::edebug('Auth method requested: ' . ($authtype ? $authtype : 'UNKNOWN'), self::DEBUG_LOWLEVEL); + self::edebug( + 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), + self::DEBUG_LOWLEVEL + ); + + if (empty($authtype)) { + foreach (array('CRAM-MD5', 'LOGIN', 'PLAIN', 'NTLM', 'XOAUTH2') as $method) { + if (in_array($method, $this->server_caps['AUTH'])) { + $authtype = $method; + break; + } + } + if (empty($authtype)) { + $this->setError('No supported authentication methods found'); + return false; + } + self::edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); + } + + if (!in_array($authtype, $this->server_caps['AUTH'])) { + $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); + return false; + } + } elseif (empty($authtype)) { + $authtype = 'LOGIN'; + } + switch ($authtype) { + case 'PLAIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { + return false; + } + // Send encoded username and password + if (!$this->sendCommand( + 'User & Password', + base64_encode("\0" . $username . "\0" . $password), + 235 + ) + ) { + return false; + } + break; + case 'LOGIN': + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { + return false; + } + if (!$this->sendCommand("Username", base64_encode($username), 334)) { + return false; + } + if (!$this->sendCommand("Password", base64_encode($password), 235)) { + return false; + } + break; + case 'XOAUTH2': + //If the OAuth Instance is not set. Can be a case when PHPMailer is used + //instead of PHPMailerOAuth + if (is_null($OAuth)) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; + } + break; + case 'NTLM': + /* + * ntlm_sasl_client.php + * Bundled with Permission + * + * How to telnet in windows: + * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx + * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication + */ + require_once 'extras/ntlm_sasl_client.php'; + $temp = new stdClass; + $ntlm_client = new ntlm_sasl_client_class; + //Check that functions are available + if (!$ntlm_client->initialize($temp)) { + $this->setError($temp->error); + $this->edebug( + 'You need to enable some modules in your php.ini file: ' + . $this->error['error'], + self::DEBUG_CLIENT + ); + return false; + } + //msg1 + $msg1 = $ntlm_client->typeMsg1($realm, $workstation); //msg1 + + if (!$this->sendCommand( + 'AUTH NTLM', + 'AUTH NTLM ' . base64_encode($msg1), + 334 + ) + ) { + return false; + } + //Though 0 based, there is a white space after the 3 digit number + //msg2 + $challenge = substr($this->last_reply, 3); + $challenge = base64_decode($challenge); + $ntlm_res = $ntlm_client->NTLMResponse( + substr($challenge, 24, 8), + $password + ); + //msg3 + $msg3 = $ntlm_client->typeMsg3( + $ntlm_res, + $username, + $realm, + $workstation + ); + // send encoded username + return $this->sendCommand('Username', base64_encode($msg3), 235); + case 'CRAM-MD5': + // Start authentication + if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { + return false; + } + // Get the challenge + $challenge = base64_decode(substr($this->last_reply, 4)); + + // Build the response + $response = $username . ' ' . $this->hmac($challenge, $password); + + // send encoded credentials + return $this->sendCommand('Username', base64_encode($response), 235); + default: + $this->setError("Authentication method \"$authtype\" is not supported"); + return false; + } + return true; + } + + /** + * Calculate an MD5 HMAC hash. + * Works like hash_hmac('md5', $data, $key) + * in case that function is not available + * @param string $data The data to hash + * @param string $key The key to hash with + * @access protected + * @return string + */ + protected function hmac($data, $key) + { + if (function_exists('hash_hmac')) { + return hash_hmac('md5', $data, $key); + } + + // The following borrowed from + // http://php.net/manual/en/function.mhash.php#27225 + + // RFC 2104 HMAC implementation for php. + // Creates an md5 HMAC. + // Eliminates the need to install mhash to compute a HMAC + // by Lance Rushing + + $bytelen = 64; // byte length for md5 + if (strlen($key) > $bytelen) { + $key = pack('H*', md5($key)); + } + $key = str_pad($key, $bytelen, chr(0x00)); + $ipad = str_pad('', $bytelen, chr(0x36)); + $opad = str_pad('', $bytelen, chr(0x5c)); + $k_ipad = $key ^ $ipad; + $k_opad = $key ^ $opad; + + return md5($k_opad . pack('H*', md5($k_ipad . $data))); + } + + /** + * Check connection state. + * @access public + * @return boolean True if connected. + */ + public function connected() + { + if (is_resource($this->smtp_conn)) { + $sock_status = stream_get_meta_data($this->smtp_conn); + if ($sock_status['eof']) { + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); + $this->close(); + return false; + } + return true; // everything looks good + } + return false; + } + + /** + * Close the socket and clean up the state of the class. + * Don't use this function without first trying to use QUIT. + * @see quit() + * @access public + * @return void + */ + public function close() + { + $this->setError(''); + $this->server_caps = null; + $this->helo_rply = null; + if (is_resource($this->smtp_conn)) { + // close the connection and cleanup + fclose($this->smtp_conn); + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug('Connection: closed', self::DEBUG_CONNECTION); + } + } + + /** + * Send an SMTP DATA command. + * Issues a data command and sends the msg_data to the server, + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a with the message headers + * and the message body being separated by and additional . + * Implements rfc 821: DATA + * @param string $msg_data Message data to send + * @access public + * @return boolean + */ + public function data($msg_data) + { + //This will use the standard timelimit + if (!$this->sendCommand('DATA', 'DATA', 354)) { + return false; + } + + /* The server is ready to accept data! + * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF) + * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into + * smaller lines to fit within the limit. + * We will also look for lines that start with a '.' and prepend an additional '.'. + * NOTE: this does not count towards line-length limit. + */ + + // Normalize line breaks before exploding + $lines = explode("\n", str_replace(array("\r\n", "\r"), "\n", $msg_data)); + + /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field + * of the first line (':' separated) does not contain a space then it _should_ be a header and we will + * process all lines before a blank line as headers. + */ + + $field = substr($lines[0], 0, strpos($lines[0], ':')); + $in_headers = false; + if (!empty($field) && strpos($field, ' ') === false) { + $in_headers = true; + } + + foreach ($lines as $line) { + $lines_out = array(); + if ($in_headers and $line == '') { + $in_headers = false; + } + //Break this line up into several smaller lines if it's too long + //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), + while (isset($line[self::MAX_LINE_LENGTH])) { + //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on + //so as to avoid breaking in the middle of a word + $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); + //Deliberately matches both false and 0 + if (!$pos) { + //No nice break found, add a hard break + $pos = self::MAX_LINE_LENGTH - 1; + $lines_out[] = substr($line, 0, $pos); + $line = substr($line, $pos); + } else { + //Break at the found point + $lines_out[] = substr($line, 0, $pos); + //Move along by the amount we dealt with + $line = substr($line, $pos + 1); + } + //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 + if ($in_headers) { + $line = "\t" . $line; + } + } + $lines_out[] = $line; + + //Send the lines to the server + foreach ($lines_out as $line_out) { + //RFC2821 section 4.5.2 + if (!empty($line_out) and $line_out[0] == '.') { + $line_out = '.' . $line_out; + } + $this->client_send($line_out . self::CRLF); + } + } + + //Message data has been sent, complete the command + //Increase timelimit for end of DATA command + $savetimelimit = $this->Timelimit; + $this->Timelimit = $this->Timelimit * 2; + $result = $this->sendCommand('DATA END', '.', 250); + //Restore timelimit + $this->Timelimit = $savetimelimit; + return $result; + } + + /** + * Send an SMTP HELO or EHLO command. + * Used to identify the sending server to the receiving server. + * This makes sure that client and server are in a known state. + * Implements RFC 821: HELO + * and RFC 2821 EHLO. + * @param string $host The host name or IP to connect to + * @access public + * @return boolean + */ + public function hello($host = '') + { + //Try extended hello first (RFC 2821) + return (boolean)($this->sendHello('EHLO', $host) or $this->sendHello('HELO', $host)); + } + + /** + * Send an SMTP HELO or EHLO command. + * Low-level implementation used by hello() + * @see hello() + * @param string $hello The HELO string + * @param string $host The hostname to say we are + * @access protected + * @return boolean + */ + protected function sendHello($hello, $host) + { + $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); + $this->helo_rply = $this->last_reply; + if ($noerror) { + $this->parseHelloFields($hello); + } else { + $this->server_caps = null; + } + return $noerror; + } + + /** + * Parse a reply to HELO/EHLO command to discover server extensions. + * In case of HELO, the only parameter that can be discovered is a server name. + * @access protected + * @param string $type - 'HELO' or 'EHLO' + */ + protected function parseHelloFields($type) + { + $this->server_caps = array(); + $lines = explode("\n", $this->helo_rply); + + foreach ($lines as $n => $s) { + //First 4 chars contain response code followed by - or space + $s = trim(substr($s, 4)); + if (empty($s)) { + continue; + } + $fields = explode(' ', $s); + if (!empty($fields)) { + if (!$n) { + $name = $type; + $fields = $fields[0]; + } else { + $name = array_shift($fields); + switch ($name) { + case 'SIZE': + $fields = ($fields ? $fields[0] : 0); + break; + case 'AUTH': + if (!is_array($fields)) { + $fields = array(); + } + break; + default: + $fields = true; + } + } + $this->server_caps[$name] = $fields; + } + } + } + + /** + * Send an SMTP MAIL command. + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. + * Implements rfc 821: MAIL FROM: + * @param string $from Source address of this message + * @access public + * @return boolean + */ + public function mail($from) + { + $useVerp = ($this->do_verp ? ' XVERP' : ''); + return $this->sendCommand( + 'MAIL FROM', + 'MAIL FROM:<' . $from . '>' . $useVerp, + 250 + ); + } + + /** + * Send an SMTP QUIT command. + * Closes the socket if there is no error or the $close_on_error argument is true. + * Implements from rfc 821: QUIT + * @param boolean $close_on_error Should the connection close if an error occurs? + * @access public + * @return boolean + */ + public function quit($close_on_error = true) + { + $noerror = $this->sendCommand('QUIT', 'QUIT', 221); + $err = $this->error; //Save any error + if ($noerror or $close_on_error) { + $this->close(); + $this->error = $err; //Restore any error from the quit command + } + return $noerror; + } + + /** + * Send an SMTP RCPT command. + * Sets the TO argument to $toaddr. + * Returns true if the recipient was accepted false if it was rejected. + * Implements from rfc 821: RCPT TO: + * @param string $address The address the message is being sent to + * @access public + * @return boolean + */ + public function recipient($address) + { + return $this->sendCommand( + 'RCPT TO', + 'RCPT TO:<' . $address . '>', + array(250, 251) + ); + } + + /** + * Send an SMTP RSET command. + * Abort any transaction that is currently in progress. + * Implements rfc 821: RSET + * @access public + * @return boolean True on success. + */ + public function reset() + { + return $this->sendCommand('RSET', 'RSET', 250); + } + + /** + * Send a command to an SMTP server and check its return code. + * @param string $command The command name - not sent to the server + * @param string $commandstring The actual command to send + * @param integer|array $expect One or more expected integer success codes + * @access protected + * @return boolean True on success. + */ + protected function sendCommand($command, $commandstring, $expect) + { + if (!$this->connected()) { + $this->setError("Called $command without being connected"); + return false; + } + //Reject line breaks in all commands + if (strpos($commandstring, "\n") !== false or strpos($commandstring, "\r") !== false) { + $this->setError("Command '$command' contained line breaks"); + return false; + } + $this->client_send($commandstring . self::CRLF); + + $this->last_reply = $this->get_lines(); + // Fetch SMTP code and possible error code explanation + $matches = array(); + if (preg_match("/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches)) { + $code = $matches[1]; + $code_ex = (count($matches) > 2 ? $matches[2] : null); + // Cut off error code from each response line + $detail = preg_replace( + "/{$code}[ -]" . + ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . "/m", + '', + $this->last_reply + ); + } else { + // Fall back to simple parsing if regex fails + $code = substr($this->last_reply, 0, 3); + $code_ex = null; + $detail = substr($this->last_reply, 4); + } + + $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); + + if (!in_array($code, (array)$expect)) { + $this->setError( + "$command command failed", + $detail, + $code, + $code_ex + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, + self::DEBUG_CLIENT + ); + return false; + } + + $this->setError(''); + return true; + } + + /** + * Send an SMTP SAML command. + * Starts a mail transaction from the email address specified in $from. + * Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. This command + * will send the message to the users terminal if they are logged + * in and send them an email. + * Implements rfc 821: SAML FROM: + * @param string $from The address the message is from + * @access public + * @return boolean + */ + public function sendAndMail($from) + { + return $this->sendCommand('SAML', "SAML FROM:$from", 250); + } + + /** + * Send an SMTP VRFY command. + * @param string $name The name to verify + * @access public + * @return boolean + */ + public function verify($name) + { + return $this->sendCommand('VRFY', "VRFY $name", array(250, 251)); + } + + /** + * Send an SMTP NOOP command. + * Used to keep keep-alives alive, doesn't actually do anything + * @access public + * @return boolean + */ + public function noop() + { + return $this->sendCommand('NOOP', 'NOOP', 250); + } + + /** + * Send an SMTP TURN command. + * This is an optional command for SMTP that this class does not support. + * This method is here to make the RFC821 Definition complete for this class + * and _may_ be implemented in future + * Implements from rfc 821: TURN + * @access public + * @return boolean + */ + public function turn() + { + $this->setError('The SMTP TURN command is not implemented'); + $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); + return false; + } + + /** + * Send raw data to the server. + * @param string $data The data to send + * @access public + * @return integer|boolean The number of bytes sent to the server or false on error + */ + public function client_send($data) + { + $this->edebug("CLIENT -> SERVER: $data", self::DEBUG_CLIENT); + return fwrite($this->smtp_conn, $data); + } + + /** + * Get the latest error. + * @access public + * @return array + */ + public function getError() + { + return $this->error; + } + + /** + * Get SMTP extensions available on the server + * @access public + * @return array|null + */ + public function getServerExtList() + { + return $this->server_caps; + } + + /** + * A multipurpose method + * The method works in three ways, dependent on argument value and current state + * 1. HELO/EHLO was not sent - returns null and set up $this->error + * 2. HELO was sent + * $name = 'HELO': returns server name + * $name = 'EHLO': returns boolean false + * $name = any string: returns null and set up $this->error + * 3. EHLO was sent + * $name = 'HELO'|'EHLO': returns server name + * $name = any string: if extension $name exists, returns boolean True + * or its options. Otherwise returns boolean False + * In other words, one can use this method to detect 3 conditions: + * - null returned: handshake was not or we don't know about ext (refer to $this->error) + * - false returned: the requested feature exactly not exists + * - positive value returned: the requested feature exists + * @param string $name Name of SMTP extension or 'HELO'|'EHLO' + * @return mixed + */ + public function getServerExt($name) + { + if (!$this->server_caps) { + $this->setError('No HELO/EHLO was sent'); + return null; + } + + // the tight logic knot ;) + if (!array_key_exists($name, $this->server_caps)) { + if ($name == 'HELO') { + return $this->server_caps['EHLO']; + } + if ($name == 'EHLO' || array_key_exists('EHLO', $this->server_caps)) { + return false; + } + $this->setError('HELO handshake was used. Client knows nothing about server extensions'); + return null; + } + + return $this->server_caps[$name]; + } + + /** + * Get the last reply from the server. + * @access public + * @return string + */ + public function getLastReply() + { + return $this->last_reply; + } + + /** + * Read the SMTP server's response. + * Either before eof or socket timeout occurs on the operation. + * With SMTP we can tell if we have more lines to read if the + * 4th character is '-' symbol. If it is a space then we don't + * need to read anything else. + * @access protected + * @return string + */ + protected function get_lines() + { + // If the connection is bad, give up straight away + if (!is_resource($this->smtp_conn)) { + return ''; + } + $data = ''; + $endtime = 0; + stream_set_timeout($this->smtp_conn, $this->Timeout); + if ($this->Timelimit > 0) { + $endtime = time() + $this->Timelimit; + } + while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { + $str = @fgets($this->smtp_conn, 515); + $this->edebug("SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL); + $this->edebug("SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL); + $data .= $str; + // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen + if ((isset($str[3]) and $str[3] == ' ')) { + break; + } + // Timed-out? Log and break + $info = stream_get_meta_data($this->smtp_conn); + if ($info['timed_out']) { + $this->edebug( + 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)', + self::DEBUG_LOWLEVEL + ); + break; + } + // Now check if reads took too long + if ($endtime and time() > $endtime) { + $this->edebug( + 'SMTP -> get_lines(): timelimit reached (' . + $this->Timelimit . ' sec)', + self::DEBUG_LOWLEVEL + ); + break; + } + } + return $data; + } + + /** + * Enable or disable VERP address generation. + * @param boolean $enabled + */ + public function setVerp($enabled = false) + { + $this->do_verp = $enabled; + } + + /** + * Get VERP address generation mode. + * @return boolean + */ + public function getVerp() + { + return $this->do_verp; + } + + /** + * Set error messages and codes. + * @param string $message The error message + * @param string $detail Further detail on the error + * @param string $smtp_code An associated SMTP error code + * @param string $smtp_code_ex Extended SMTP code + */ + protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '') + { + $this->error = array( + 'error' => $message, + 'detail' => $detail, + 'smtp_code' => $smtp_code, + 'smtp_code_ex' => $smtp_code_ex + ); + } + + /** + * Set debug output method. + * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it. + */ + public function setDebugOutput($method = 'echo') + { + $this->Debugoutput = $method; + } + + /** + * Get debug output method. + * @return string + */ + public function getDebugOutput() + { + return $this->Debugoutput; + } + + /** + * Set debug output level. + * @param integer $level + */ + public function setDebugLevel($level = 0) + { + $this->do_debug = $level; + } + + /** + * Get debug output level. + * @return integer + */ + public function getDebugLevel() + { + return $this->do_debug; + } + + /** + * Set SMTP timeout. + * @param integer $timeout + */ + public function setTimeout($timeout = 0) + { + $this->Timeout = $timeout; + } + + /** + * Get SMTP timeout. + * @return integer + */ + public function getTimeout() + { + return $this->Timeout; + } + + /** + * Reports an error number and string. + * @param integer $errno The error number returned by PHP. + * @param string $errmsg The error message returned by PHP. + * @param string $errfile The file the error occurred in + * @param integer $errline The line number the error occurred on + */ + protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0) + { + $notice = 'Connection failed.'; + $this->setError( + $notice, + $errno, + $errmsg + ); + $this->edebug( + $notice . ' Error #' . $errno . ': ' . $errmsg . " [$errfile line $errline]", + self::DEBUG_CONNECTION + ); + } + + /** + * Will return the ID of the last smtp transaction based on a list of patterns provided + * in SMTP::$smtp_transaction_id_patterns. + * If no reply has been received yet, it will return null. + * If no pattern has been matched, it will return false. + * @return bool|null|string + */ + public function getLastTransactionID() + { + $reply = $this->getLastReply(); + + if (empty($reply)) { + return null; + } + + foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { + if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) { + return $matches[1]; + } + } + + return false; + } +} -- cgit v1.2.3 From 5d43957594589143f0257a918a99b4d6977bdafc Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 00:37:04 -0400 Subject: Add global function sendEmail() Helper routine to spin up a PHPMailer object, set all its options (mostly from the database) and send off the message. --- app/class/globals.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 615efa6..a84ed39 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -12,6 +12,10 @@ * For more information, please refer to UNLICENSE */ +require_once "class/settings.class.php"; +require_once "class/phpmailer.class.php"; +require_once "class/smtp.class.php"; + /* * This file defines various functions which exist in the global namespace. * These are utility functions and constants for the Scrott application. @@ -144,4 +148,34 @@ function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = return true; } +/* + * Send an email message using database-stored configuration + * parameters. If config is not established, delivery is + * not attempted. Send status (t/f) is returned. + */ +function sendEmail(string $subject, array $rcpt, string $mesg) : bool +{ + if (settings::smtpServer() == "") + return false; + + $mail = new PHPMailer(); + $mail->isSMTP(); + $mail->SMTPAuth = true; + $mail->Host = settings::smtpServer(); + $mail->Username = settings::smtpUname(); + $mail->Password = settings::smtpPasswd(); + $mail->SMTPSecure = settings::smtpSecurity(); + $mail->Port = settings::smtpPort(); + + $mail->setFrom(settings::smtpEmailAddress()); + + foreach ($rcpt as $to) + $mail->addAddress($to); + + $mail->Subject = $subject; + $mail->Body = $mesg; + + return $mail->send(); +} + ?> -- cgit v1.2.3 From 4211ae67018059407830e60ec4199c80320b6dc3 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 00:48:02 -0400 Subject: Update global function sendEmail() Function should be only sending to one recipient at a time. --- app/class/globals.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index a84ed39..e4285cd 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -153,7 +153,7 @@ function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = * parameters. If config is not established, delivery is * not attempted. Send status (t/f) is returned. */ -function sendEmail(string $subject, array $rcpt, string $mesg) : bool +function sendEmail(string $subject, string $rcpt, string $mesg) : bool { if (settings::smtpServer() == "") return false; @@ -168,10 +168,7 @@ function sendEmail(string $subject, array $rcpt, string $mesg) : bool $mail->Port = settings::smtpPort(); $mail->setFrom(settings::smtpEmailAddress()); - - foreach ($rcpt as $to) - $mail->addAddress($to); - + $mail->addAddress($rcpt); $mail->Subject = $subject; $mail->Body = $mesg; -- cgit v1.2.3 From 3fea275e10abf8281088e9c7f0ca316df351e41f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 01:32:16 -0400 Subject: Update global function sendEmail() Changing the $rcpt argument from an email address string to a user object. This allows us to ensure the address has been confirmed, to not send mail to a blank address, and to include the user's display name in the TO mail headers. Also, added support for mail attachments via PHPMailer. This can be used to forward any attachments added to Scrott message objects to email users as well. --- app/class/globals.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index e4285cd..0d4b0ac 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -153,11 +153,19 @@ function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = * parameters. If config is not established, delivery is * not attempted. Send status (t/f) is returned. */ -function sendEmail(string $subject, string $rcpt, string $mesg) : bool +function sendEmail(string $subject, user $rcpt, string $mesg, + ?string $attachPath = NULL, ?string $attachName = NULL, + bool $overrideEmailConf = false) : bool { if (settings::smtpServer() == "") return false; + if (!$overrideEmailConf && !$rcpt->emailConf) + return true; + + if ($rcpt->email == "") + return true; + $mail = new PHPMailer(); $mail->isSMTP(); $mail->SMTPAuth = true; @@ -168,10 +176,13 @@ function sendEmail(string $subject, string $rcpt, string $mesg) : bool $mail->Port = settings::smtpPort(); $mail->setFrom(settings::smtpEmailAddress()); - $mail->addAddress($rcpt); + $mail->addAddress($rcpt->email, $rcpt->getDisplayName()); $mail->Subject = $subject; $mail->Body = $mesg; + if ($attachPath && $attachName) + $mail->addAttachment($attachPath, $attachName); + return $mail->send(); } -- cgit v1.2.3 From fed99e2d0938b10018c8264632165aad56bc2561 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 12:19:02 -0400 Subject: Move sendEmail() function into agent class Adding this as an abstract function to class agent. Since we will only be sending emails to stored users (and groups) this makes more sense and allows us to remove this function from the global namespace as well. --- app/class/agent.class.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index a2c8c2e..ed50b93 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -14,6 +14,9 @@ require_once "class/object.class.php"; require_once "class/pad.class.php"; +require_once "class/settings.class.php"; +require_once "class/phpmailer.class.php"; +require_once "class/smtp.class.php"; /* * This is a supertype for users and groups, since these two object types @@ -51,6 +54,15 @@ abstract class agent extends object return false; } + /* + * Send an email message to this agent using stored configuration + * parameters. If config is not established, delivery is not + * attempted. Return status. + */ + public abstract function sendEmail(string $subj, string $mesg, + ?string $attachPath = NULL, ?string $attachName = NULL, + bool $ignoreEmailConf = false) : bool; + /* * Get the display name for this agent. For groups this is the * object name; for users, this is the object name, unless an -- cgit v1.2.3 From 05c33eb8da0c926c07288ddb07821fb967e20d7d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 12:23:26 -0400 Subject: Implement function sendEmail() for user class --- app/class/user.class.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index a6addf6..01552f6 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -282,6 +282,45 @@ class user extends agent return $groups; } + + /* + * Send an email message to this user using stored configuration + * parameters. If config is not established, delivery is not + * attempted. Return status. + */ + public function sendEmail(string $subj, string $mesg, + ?string $attachPath = NULL, ?string $attachName = NULL, + bool $ignoreEmailConf = false) : bool + { + if (settings::smtpServer() == "") + return false; + + if (!$ignoreEmailConf && !$this->emailConf) + return true; + + if ($this->email == "") + return true; + + $mail = new PHPMailer(); + $mail->isSMTP(); + $mail->SMTPAuth = true; + + $mail->Host = settings::smtpServer(); + $mail->Port = settings::smtpPort(); + $mail->Username = settings::smtpUname(); + $mail->Password = settings::smtpPasswd(); + $mail->SMTPSecure = settings::smtpSecurity(); + + $mail->setFrom(settings::smtpEmailAddress()); + $mail->addAddress($this->email, $this->getDisplayName()); + $mail->Subject = $subj; + $mail->Body = $mesg; + + if ($attachPath && $attachName) + $mail->addAttachment($attachPath, $attachName); + + return $mail->send(); + } } ?> -- cgit v1.2.3 From 613ef13e809c3f083a76797c7706935d3bbfcb29 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 12:29:02 -0400 Subject: Revert "Add global function sendEmail()" This reverts commit 45889e98e7a12b22cbaaceedd5531d4158888530. This reverts commit 6b643d4bbb469d35c6664176bc1aa641d130d99f. This reverts commit 7872377be7a0fc97316fc20d28a4bcfec15c6111. This feature was moved to another file. --- app/class/globals.php | 42 ------------------------------------------ 1 file changed, 42 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 0d4b0ac..615efa6 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -12,10 +12,6 @@ * For more information, please refer to UNLICENSE */ -require_once "class/settings.class.php"; -require_once "class/phpmailer.class.php"; -require_once "class/smtp.class.php"; - /* * This file defines various functions which exist in the global namespace. * These are utility functions and constants for the Scrott application. @@ -148,42 +144,4 @@ function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = return true; } -/* - * Send an email message using database-stored configuration - * parameters. If config is not established, delivery is - * not attempted. Send status (t/f) is returned. - */ -function sendEmail(string $subject, user $rcpt, string $mesg, - ?string $attachPath = NULL, ?string $attachName = NULL, - bool $overrideEmailConf = false) : bool -{ - if (settings::smtpServer() == "") - return false; - - if (!$overrideEmailConf && !$rcpt->emailConf) - return true; - - if ($rcpt->email == "") - return true; - - $mail = new PHPMailer(); - $mail->isSMTP(); - $mail->SMTPAuth = true; - $mail->Host = settings::smtpServer(); - $mail->Username = settings::smtpUname(); - $mail->Password = settings::smtpPasswd(); - $mail->SMTPSecure = settings::smtpSecurity(); - $mail->Port = settings::smtpPort(); - - $mail->setFrom(settings::smtpEmailAddress()); - $mail->addAddress($rcpt->email, $rcpt->getDisplayName()); - $mail->Subject = $subject; - $mail->Body = $mesg; - - if ($attachPath && $attachName) - $mail->addAttachment($attachPath, $attachName); - - return $mail->send(); -} - ?> -- cgit v1.2.3 From b8db87d3b814b1845e8282c066a895d9dcb5a0a9 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 6 Jun 2017 13:20:31 -0400 Subject: Implement function sendEmail() for group class --- app/class/group.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'app/class') diff --git a/app/class/group.class.php b/app/class/group.class.php index 5163cb8..1191d71 100644 --- a/app/class/group.class.php +++ b/app/class/group.class.php @@ -62,6 +62,32 @@ class group extends agent $group->saveObj(); return $group; } + + /* + * Send an email message to this group using stored configuration + * parameters. If config is not established, delivery is not + * attempted. Return status. If any delivery attempts fail, the + * rest are aborted and false is returned. + */ + public function sendEmail(string $subj, string $mesg, + ?string $attachPath = NULL, ?string $attachName = NULL, + bool $ignoreEmailConf = false) : bool + { + $owner = $this->getOwner(); + + if (!$owner->sendEmail($subj, $mesg, $attachPath, $attachName, + $ignoreEmailConf)) + return false; + + foreach ($this->getMembers() as $memb) + { + if (!$memb->sendEmail($subj, $mesg, $attachPath, $attachName, + $ignoreEmailConf)) + return false; + } + + return true; + } } ?> -- cgit v1.2.3 From fa929cbd64d662126b8734e476bdbbf3562333d4 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Jun 2017 21:56:58 -0400 Subject: Add issue function getAssignee() --- app/class/issue.class.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 6056457..c3381a7 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -62,6 +62,17 @@ class issue extends object return $issue; } + /* + * Get the assignee for this issue + */ + public function getAssignee() : ?user + { + if (!isset($this->assignee) || $this->assignee == "") + return NULL; + + return new user($this->assignee); + } + /* * Reset the seen flag and reassign this issue. */ -- cgit v1.2.3 From d840a3d9b464ac424d210aeffc01cea0a774377b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Jun 2017 22:03:24 -0400 Subject: Add issue function getPad() --- app/class/issue.class.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index c3381a7..b61a6e3 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -83,6 +83,19 @@ class issue extends object $this->saveObj(); } + /* + * Get the pad this issue exists under + */ + public function getPad() : pad + { + $parent = $this->getParent(); + + if ($parent->objtype == "pad") + return $parent; + + return $parent->getParent(); + } + /* * Advance this issue in the pipeline, closing it if already in the * last stage. -- cgit v1.2.3 From d178d05272863bb8d920a993b7d6fc4b1f275dcf Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Jun 2017 22:19:11 -0400 Subject: Add object function arrayUnique() --- app/class/object.class.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index 461f1b1..c1ba85c 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -70,6 +70,32 @@ class object extends table return $obj->objtype; } + /* + * Remove duplicate elements from an array of Scrott objects. This + * function compares object GUIDs to check for uniqueness. Array + * keys are preserved. NULL elements are removed. Resulting array + * is returned. + */ + public static function arrayUnique(array $arr) : array + { + $guids = array(); + $ret = array(); + + foreach ($arr as $k => $v) + { + if ($v === NULL) + continue; + + if (in_array($v->guid, $guids)) + continue; + + $guids[] = $v->guid; + $ret[$k] = $v; + } + + return $ret; + } + /* * Get the owner of this object. Either a user object or a group * object will be returned. If this object does not have an owner, -- cgit v1.2.3 From 1162f87e4326868160cfddaa524efd1386b34133 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 13 Jun 2017 22:35:17 -0400 Subject: Add mesg function emailMesg() --- app/class/mesg.class.php | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index 6c84d4f..da52fa6 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -282,6 +282,56 @@ class mesg extends object $this->delObj(); return $issue; } + + /* + * Email this message to parents, owners, members. In the case that + * this is an issue message or a reply message, the assignee or original + * author is also included. Attachments are included in mailing. Any + * duplicates in the rcpt list are removed before sending. Success + * or failure is returned. + */ + public function emailMesg() : bool + { + $parent = $this->getParent(); + + if (!$parent) + return true; + + $rcpt = $this->getMembers(); + $rcpt[] = $this->getOwner(); + + switch ($parent->objtype) + { + case "user": + $rcpt[] = $parent; + $subj = $this->author . " PM from " . $this->getAuthor()->getDisplayName(); + break; + + case "issue": + $rcpt[] = $parent->getAssignee(); + $pad = $parent->getPad(); + $subj = $parent->guid . " " . $pad->name . " [#" . $parent->numb . "] " . $parent->name; + break; + + case "mesg": + case "log": + $rcpt[] = $parent->getAuthor(); + $pad = $parent->getParent(); + $subj = $parent->guid . " " . $pad->name . " // " . $parent->name; + break; + } + + $rcpt = object::arrayUnique($rcpt); + $attachPath = ($this->getAttachment() ? "dynmic/attach/" . $this->guid : NULL); + + foreach ($rcpt as $r) + { + if (!$r->sendEmail($subj, $this->mesg, $attachPath, $this->attachment)) + return false; + } + + return true; + } } ?> -- cgit v1.2.3 From 9fd988bf44089634bd3efa8acc95368cf45c2498 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 14 Jun 2017 00:53:13 -0400 Subject: Update mesg function emailMesg() Fixed a bug and fine-tuned some of the behavior of this function. --- app/class/mesg.class.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index da52fa6..2512d03 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -297,14 +297,14 @@ class mesg extends object if (!$parent) return true; - $rcpt = $this->getMembers(); - $rcpt[] = $this->getOwner(); + $rcpt = $parent->getMembers(); + $rcpt[] = $parent->getOwner(); switch ($parent->objtype) { case "user": $rcpt[] = $parent; - $subj = $this->author . " PM from " . $this->getAuthor()->getDisplayName(); + $subj = $this->author . " " . $this->getAuthor()->getDisplayName() . " // " . $this->name; break; case "issue": @@ -322,11 +322,15 @@ class mesg extends object } $rcpt = object::arrayUnique($rcpt); + $author = $this->author; + $rcpt = array_filter($rcpt, function ($val) use($author) { return $val->guid != $author; }); $attachPath = ($this->getAttachment() ? "dynmic/attach/" . $this->guid : NULL); + $mesg = $this->getAuthor()->getDisplayName() . " wrote on " . $this->created . "\n\n"; + $mesg .= $this->renderMesg(); foreach ($rcpt as $r) { - if (!$r->sendEmail($subj, $this->mesg, $attachPath, $this->attachment)) + if (!$r->sendEmail($subj, $mesg, $attachPath, $this->attachment)) return false; } -- cgit v1.2.3 From ae71ff561fb0e469b00dbe0942144f8a0b771246 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 19 Jun 2017 23:35:30 -0400 Subject: Add setting parameter 'smtpFrom' This is the name to give on FROM headers to generated email messages. --- app/class/settings.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/settings.class.php b/app/class/settings.class.php index 5b7cdeb..19fba6a 100644 --- a/app/class/settings.class.php +++ b/app/class/settings.class.php @@ -98,6 +98,14 @@ abstract class settings return self::option("smtpEmailAddress", "", $value); } + /* + * SMTP FROM name + */ + public static function smtpFrom(?string $value = NULL) : string + { + return self::option("smtpFrom", "", $value); + } + /* * SMTP server address */ -- cgit v1.2.3 From 2564b9953bb7bd8e90a9865962cb9e88d4cfd218 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 19 Jun 2017 23:45:03 -0400 Subject: Update function user::sendEmail() Now setting the name for email FROM field using system config 'smtpFrom'. --- app/class/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 01552f6..81fc29f 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -311,7 +311,7 @@ class user extends agent $mail->Password = settings::smtpPasswd(); $mail->SMTPSecure = settings::smtpSecurity(); - $mail->setFrom(settings::smtpEmailAddress()); + $mail->setFrom(settings::smtpEmailAddress(), settings::smtpFrom()); $mail->addAddress($this->email, $this->getDisplayName()); $mail->Subject = $subj; $mail->Body = $mesg; -- cgit v1.2.3 From 4a9908d9aa3d0f50d4907eb25e6fc1d317061484 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 27 Jun 2017 01:35:26 -0400 Subject: Add form class --- app/class/form.class.php | 179 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 app/class/form.class.php (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php new file mode 100644 index 0000000..8f9d936 --- /dev/null +++ b/app/class/form.class.php @@ -0,0 +1,179 @@ +textFields[] = array( + 'name' => $name, + 'required' => $required, + 'default' => $deflt, + ); + } + + /* + * Add new numeric field to this form + */ + public function numeric(string $name, ?int $min = NULL, ?int $max = NULL, + int $deflt = 0, bool $isInt = true, bool $required = true) : void + { + $this->numbFields[] = array( + 'name' => $name, + 'min' => $min, + 'max' => $max, + 'default' => $deflt, + 'isInt' => $isInt, + 'required' => $required, + ); + } + + /* + * Add new enumeration field to this form + */ + public function enum(string $name, array $values, $deflt = NULL, + bool $required = true) : void + { + $this->enumFields[] = array( + 'name' => $name, + 'values' => $values, + 'default' => $deflt, + 'required' => $required, + ); + } + + /* + * Add new boolean field to this form + */ + public function flag(string $name) : void + { + $this->enum($name, array("1", "0"), "0", false); + } + + /* + * Populate this form with input data from web page. If an error + * is encountered, or a required field is missing, false is returned. + */ + public function populate(array $input) : bool + { + /* detect duplicate names */ + $names = array(); + + foreach ($this->textFields as $field) + $names[] = $field['name']; + + foreach ($this->numbFields as $field) + $names[] = $field['name']; + + foreach ($this->enumFields as $field) + $names[] = $field['name']; + + if (count(array_unique($names)) != count($names)) + throw new Exception("Internal error: Duplicate field names defined in form"); + + /* init text fields */ + foreach ($this->textFields as $field) + { + if (isset($input[$field['name']]) && $input[$field['name']] != "") + $this->{$field['name']} = htmlentities($input[$field['name']], ENT_QUOTES); + + else if ($field['required']) + logError(ERROR, $field['name'] . " is required"); + + else + $this->{$field['name']} = $field['default']; + } + + /* init numeric fields */ + foreach ($this->numbFields as $field) + { + if (isset($input[$field['name']]) && $input[$field['name']] != "") + { + if (!is_numeric($input[$field['name']])) + { + logError(ERROR, $field['name'] . " must be numeric"); + continue; + } + + if ($field['isInt'] && (floor($input[$field['name']]) != $input[$field['name']])) + { + logError(ERROR, $field['name'] . " must be an integer"); + continue; + } + + if (!is_null($field['min']) && ($input[$field['name']] < $field['min'])) + { + logError(ERROR, $field['name'] . " must be no less than " . $field['min']); + continue; + } + + if (!is_null($field['max']) && ($input[$field['name']] > $field['max'])) + { + logError(ERROR, $field['name'] . " must be no more than " . $field['max']); + continue; + } + + $this->{$field['name']} = $input[$field['name']]; + } + + else if ($field['required']) + logError(ERROR, $field['name'] . " is required"); + + else + $this->{$field['name']} = $field['default']; + } + + /* init enum fields */ + foreach ($this->enumFields as $field) + { + if (isset($input[$field['name']]) && $input[$field['name']] != "") + { + if (array_search($input[$field['name']], $field['values']) === false) + { + logError(ERROR, $field['name'] . " is not an appropriate value"); + continue; + } + + $this->{$field['name']} = $input[$field['name']]; + } + + else if ($field['required']) + logError(ERROR, $field['name'] . " is required"); + + else + $this->{$field['name']} = $field['default']; + } + + return !isError(ERROR); + } +} + +?> -- cgit v1.2.3 From a607d74c3080cff5a9a9f4362f6da491982e8a6e Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 27 Jun 2017 01:39:02 -0400 Subject: Add function isAction() --- app/class/globals.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 615efa6..94df716 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -76,6 +76,17 @@ function require_https() : void redirect("https://" . $_SERVER['SERVER_NAME'] . ap()); } +/* + * Check whether an action string was submitted by the user agent + */ +function isAction(string $action) : bool +{ + if (!isset($_REQUEST['action'])) + return false; + + return $_REQUEST['action'] == $action; +} + /* * Check for errors, warnings, or notices */ -- cgit v1.2.3 From 09f403582c5de5dcc97b265e8ae42469c9c8ff3f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 27 Jun 2017 01:43:16 -0400 Subject: Add function input() --- app/class/globals.php | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 94df716..776fc35 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -87,6 +87,17 @@ function isAction(string $action) : bool return $_REQUEST['action'] == $action; } +/* + * Get an array of submitted form input + */ +function input() : array +{ + if (!isset($_REQUEST['input'])) + throw new Exception("Requested form input, but no input data was supplied"); + + return $_REQUEST['input']; +} + /* * Check for errors, warnings, or notices */ -- cgit v1.2.3 From be0b63cae463f814aa7eef879c0994b8e3ca16ba Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 6 Jul 2017 23:41:17 -0400 Subject: Add function database::setConfig() --- app/class/database.class.php | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index cdfdfce..3d94e16 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -115,6 +115,57 @@ abstract class database return true; } + + /* + * Test and set new database configuration parameters. + * If the given params fail, error's are set and this + * function returns false. On success, parameters are + * written to 'dbconfig.php' and true is returned. + */ + public static function setConfig(string $engine, string $host, + string $uname, string $passwd, string $name) : bool + { + global $_SCROTT; + + /* test configuration */ + $_SCROTT['conf'] = "conf"; + $_SCROTT['dbEngine'] = $engine; + $_SCROTT['dbHost'] = $host; + $_SCROTT['dbUname'] = $uname; + $_SCROTT['dbPasswd'] = $passwd; + $_SCROTT['dbName'] = $name; + + try + { + $db = self::getInstance(); + } + catch (Exception $e) + { + logError(ERROR, $e->getMessage()); + return false; + } + + /* write file */ + $f = fopen(DATABASE_CONFIG_FILE, "w"); + + if (!$f) + { + logError(ERROR, "Can not create configuration file"); + return false; + } + + fwrite($f, "\n"); + + fclose($f); + return true; + } } ?> -- cgit v1.2.3 From c283905537ffb770c2b7e85529238cca9aa96869 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 7 Feb 2018 21:24:29 -0500 Subject: Rename object class Since 'object' is now a reserved word (as of PHP 7.2), I have to rename this class. I really preferred the name object, but obj will have to do. --- app/class/object.class.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'app/class') diff --git a/app/class/object.class.php b/app/class/object.class.php index c1ba85c..5be9ac3 100644 --- a/app/class/object.class.php +++ b/app/class/object.class.php @@ -19,7 +19,7 @@ require_once "class/image.php"; * This is a generic database object. This is a supertype of all Scrott * datatypes and defines fields common to all of them. */ -class object extends table +class obj extends table { /* * Constants used for uploading images @@ -66,7 +66,7 @@ class object extends table */ public static function typeOf(string $guid) : string { - $obj = new object($guid); + $obj = new obj($guid); return $obj->objtype; } @@ -125,19 +125,19 @@ class object extends table * Get the parent of this object. If this object does not have a * parent, NULL will be returned. */ - public function getParent() : ?object + public function getParent() : ?obj { if (!isset($this->parent) || $this->parent == "") return NULL; - $parent = new object($this->parent); + $parent = new obj($this->parent); return new $parent->objtype($parent->guid); } /* * Update the parent of this object */ - public function setParent(object $parent) : void + public function setParent(obj $parent) : void { $this->parent = $parent->guid; $this->saveObj(); -- cgit v1.2.3 From 977e4271c6f27ed2d42fab6bb2706f4a59bf0237 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 7 Feb 2018 21:28:08 -0500 Subject: Rename object.class.php to reflect name of its class --- app/class/obj.class.php | 286 +++++++++++++++++++++++++++++++++++++++++++++ app/class/object.class.php | 286 --------------------------------------------- 2 files changed, 286 insertions(+), 286 deletions(-) create mode 100644 app/class/obj.class.php delete mode 100644 app/class/object.class.php (limited to 'app/class') diff --git a/app/class/obj.class.php b/app/class/obj.class.php new file mode 100644 index 0000000..5be9ac3 --- /dev/null +++ b/app/class/obj.class.php @@ -0,0 +1,286 @@ +fields['objects'] = array( + "guid", + "owner", + "parent", + "name", + "created", + "updated", + + "membModify", + "membMemb", + "membAccs", + "membCres", + "membModifys", + "membMembs", + "pubAcc", + "pubAccs", + "pubCres", + + "objtype", + ); + + parent::__construct($guid); + } + + /* + * Get the object type for the given GUID + */ + public static function typeOf(string $guid) : string + { + $obj = new obj($guid); + return $obj->objtype; + } + + /* + * Remove duplicate elements from an array of Scrott objects. This + * function compares object GUIDs to check for uniqueness. Array + * keys are preserved. NULL elements are removed. Resulting array + * is returned. + */ + public static function arrayUnique(array $arr) : array + { + $guids = array(); + $ret = array(); + + foreach ($arr as $k => $v) + { + if ($v === NULL) + continue; + + if (in_array($v->guid, $guids)) + continue; + + $guids[] = $v->guid; + $ret[$k] = $v; + } + + return $ret; + } + + /* + * Get the owner of this object. Either a user object or a group + * object will be returned. If this object does not have an owner, + * NULL will be returned. + */ + public function getOwner() : ?agent + { + if (!isset($this->owner) || $this->owner == "") + return NULL; + + if (self::typeOf($this->owner) == "group") + return new group($this->owner); + + return new user($this->owner); + } + + /* + * Update the owner of this object + */ + public function setOwner(agent $owner) : void + { + $this->owner = $owner->guid; + $this->saveObj(); + } + + /* + * Get the parent of this object. If this object does not have a + * parent, NULL will be returned. + */ + public function getParent() : ?obj + { + if (!isset($this->parent) || $this->parent == "") + return NULL; + + $parent = new obj($this->parent); + return new $parent->objtype($parent->guid); + } + + /* + * Update the parent of this object + */ + public function setParent(obj $parent) : void + { + $this->parent = $parent->guid; + $this->saveObj(); + } + + /* + * Get an array of all members of this object + */ + public function getMembers() : array + { + $memb = array(); + $query = "SELECT member FROM members WHERE guid = '" . database::esc($this->guid) . "'"; + $res = database::query($query); + + foreach ($res as $m) + $memb[] = new user($m['member']); + + return $memb; + } + + /* + * Add a user as a member of this object. Returns false if user is + * already a member, or if another error occurs; true otherwise. + */ + public function addMember(user $user) : bool + { + if ($user->isMemberOf($this) || !isset($user->guid)) + return false; + + $query = "INSERT INTO members (guid, member) VALUES ('" . database::esc($this->guid) . "', '" . + database::esc($user->guid) . "')"; + database::query($query); + return true; + } + + /* + * Remove a user as a member of this object. Returns false if user + * is not a member, or if another error occurs; true otherwise. + */ + public function remMember(user $user) : bool + { + if (!$user->isMemberOf($this) || !isset($user->guid)) + return false; + + $query = "DELETE FROM members WHERE guid = '" . database::esc($this->guid) . "' AND " . + "member = '" . database::esc($user->guid) . "'"; + database::query($query); + return true; + } + + /* + * Get all messages on this object. Messages are sorted by date + * created. + */ + public function getMesgs_ordByDatetime() : array + { + $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . + "parent = '" . database::esc($this->guid) . "' ORDER BY created"; + $res = database::query($query); + + $mesgs = array(); + + foreach ($res as $m) + $mesgs[] = new mesg($m['guid']); + + return $mesgs; + } + + /* + * Get the URL to the head image resource for this object + */ + public function getHeadImg() : string + { + return ar() . "/df.php?d=heads&f=" . $this->guid; + } + + /* + * Set the head image for this object, overwriting any existing + * image. $image should be an uploaded file to PHP, still + * unhandled. + */ + public function setHeadImg(array $image) : bool + { + $path = "dynmic/heads/" . $this->guid; + + if (!saveFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) + return false; + + if (!imageSquareCrop($path)) + { + $this->rmHeadImg(); + return false; + } + + return true; + } + + /* + * Remove the head image for this object. This deletes the image + * on disk. + */ + public function rmHeadImg() : bool + { + if (!is_file("dynmic/heads/" . $this->guid)) + return true; + + return unlink("dynmic/heads/" . $this->guid); + } + + /* + * Get the URL to the background image resource for this + * object. If no image is set, NULL is returned. + */ + public function getBgImg() : ?string + { + if (!is_file("dynmic/bgs/" . $this->guid)) + return NULL; + + return ar() . "/df.php?d=bgs&f=" . $this->guid; + } + + /* + * Set the background image for this object, overwriting any + * existing image. $image should be an uploaded file to PHP, + * still unhandled. + */ + public function setBgImg(array $image) : bool + { + $path = "dynmic/bgs/" . $this->guid; + return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); + } + + /* + * Remove the background image for this object. This deletes + * the image on disk. + */ + public function rmBgImg() : bool + { + if (!is_file("dynmic/bgs/" . $this->guid)) + return true; + + return unlink("dynmic/bgs/" . $this->guid); + } +} + +?> diff --git a/app/class/object.class.php b/app/class/object.class.php deleted file mode 100644 index 5be9ac3..0000000 --- a/app/class/object.class.php +++ /dev/null @@ -1,286 +0,0 @@ -fields['objects'] = array( - "guid", - "owner", - "parent", - "name", - "created", - "updated", - - "membModify", - "membMemb", - "membAccs", - "membCres", - "membModifys", - "membMembs", - "pubAcc", - "pubAccs", - "pubCres", - - "objtype", - ); - - parent::__construct($guid); - } - - /* - * Get the object type for the given GUID - */ - public static function typeOf(string $guid) : string - { - $obj = new obj($guid); - return $obj->objtype; - } - - /* - * Remove duplicate elements from an array of Scrott objects. This - * function compares object GUIDs to check for uniqueness. Array - * keys are preserved. NULL elements are removed. Resulting array - * is returned. - */ - public static function arrayUnique(array $arr) : array - { - $guids = array(); - $ret = array(); - - foreach ($arr as $k => $v) - { - if ($v === NULL) - continue; - - if (in_array($v->guid, $guids)) - continue; - - $guids[] = $v->guid; - $ret[$k] = $v; - } - - return $ret; - } - - /* - * Get the owner of this object. Either a user object or a group - * object will be returned. If this object does not have an owner, - * NULL will be returned. - */ - public function getOwner() : ?agent - { - if (!isset($this->owner) || $this->owner == "") - return NULL; - - if (self::typeOf($this->owner) == "group") - return new group($this->owner); - - return new user($this->owner); - } - - /* - * Update the owner of this object - */ - public function setOwner(agent $owner) : void - { - $this->owner = $owner->guid; - $this->saveObj(); - } - - /* - * Get the parent of this object. If this object does not have a - * parent, NULL will be returned. - */ - public function getParent() : ?obj - { - if (!isset($this->parent) || $this->parent == "") - return NULL; - - $parent = new obj($this->parent); - return new $parent->objtype($parent->guid); - } - - /* - * Update the parent of this object - */ - public function setParent(obj $parent) : void - { - $this->parent = $parent->guid; - $this->saveObj(); - } - - /* - * Get an array of all members of this object - */ - public function getMembers() : array - { - $memb = array(); - $query = "SELECT member FROM members WHERE guid = '" . database::esc($this->guid) . "'"; - $res = database::query($query); - - foreach ($res as $m) - $memb[] = new user($m['member']); - - return $memb; - } - - /* - * Add a user as a member of this object. Returns false if user is - * already a member, or if another error occurs; true otherwise. - */ - public function addMember(user $user) : bool - { - if ($user->isMemberOf($this) || !isset($user->guid)) - return false; - - $query = "INSERT INTO members (guid, member) VALUES ('" . database::esc($this->guid) . "', '" . - database::esc($user->guid) . "')"; - database::query($query); - return true; - } - - /* - * Remove a user as a member of this object. Returns false if user - * is not a member, or if another error occurs; true otherwise. - */ - public function remMember(user $user) : bool - { - if (!$user->isMemberOf($this) || !isset($user->guid)) - return false; - - $query = "DELETE FROM members WHERE guid = '" . database::esc($this->guid) . "' AND " . - "member = '" . database::esc($user->guid) . "'"; - database::query($query); - return true; - } - - /* - * Get all messages on this object. Messages are sorted by date - * created. - */ - public function getMesgs_ordByDatetime() : array - { - $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . - "parent = '" . database::esc($this->guid) . "' ORDER BY created"; - $res = database::query($query); - - $mesgs = array(); - - foreach ($res as $m) - $mesgs[] = new mesg($m['guid']); - - return $mesgs; - } - - /* - * Get the URL to the head image resource for this object - */ - public function getHeadImg() : string - { - return ar() . "/df.php?d=heads&f=" . $this->guid; - } - - /* - * Set the head image for this object, overwriting any existing - * image. $image should be an uploaded file to PHP, still - * unhandled. - */ - public function setHeadImg(array $image) : bool - { - $path = "dynmic/heads/" . $this->guid; - - if (!saveFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) - return false; - - if (!imageSquareCrop($path)) - { - $this->rmHeadImg(); - return false; - } - - return true; - } - - /* - * Remove the head image for this object. This deletes the image - * on disk. - */ - public function rmHeadImg() : bool - { - if (!is_file("dynmic/heads/" . $this->guid)) - return true; - - return unlink("dynmic/heads/" . $this->guid); - } - - /* - * Get the URL to the background image resource for this - * object. If no image is set, NULL is returned. - */ - public function getBgImg() : ?string - { - if (!is_file("dynmic/bgs/" . $this->guid)) - return NULL; - - return ar() . "/df.php?d=bgs&f=" . $this->guid; - } - - /* - * Set the background image for this object, overwriting any - * existing image. $image should be an uploaded file to PHP, - * still unhandled. - */ - public function setBgImg(array $image) : bool - { - $path = "dynmic/bgs/" . $this->guid; - return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); - } - - /* - * Remove the background image for this object. This deletes - * the image on disk. - */ - public function rmBgImg() : bool - { - if (!is_file("dynmic/bgs/" . $this->guid)) - return true; - - return unlink("dynmic/bgs/" . $this->guid); - } -} - -?> -- cgit v1.2.3 From e54418762e279a1d7ca0efb7ed89b95464753ee8 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 7 Feb 2018 22:33:19 -0500 Subject: Update class files to use renamed obj class --- app/class/agent.class.php | 62 +++++++++++++++++++++++------------------------ app/class/issue.class.php | 4 +-- app/class/mesg.class.php | 10 ++++---- app/class/pad.class.php | 4 +-- app/class/stage.class.php | 6 ++--- 5 files changed, 43 insertions(+), 43 deletions(-) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index ed50b93..6d0e20d 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -12,7 +12,7 @@ * For more information, please refer to UNLICENSE */ -require_once "class/object.class.php"; +require_once "class/obj.class.php"; require_once "class/pad.class.php"; require_once "class/settings.class.php"; require_once "class/phpmailer.class.php"; @@ -22,7 +22,7 @@ require_once "class/smtp.class.php"; * This is a supertype for users and groups, since these two object types * will often be handled polymorphically and will share some functionality. */ -abstract class agent extends object +abstract class agent extends obj { /* * Constructor @@ -35,7 +35,7 @@ abstract class agent extends object /* * Check whether this agent is the owner of the given object */ - public function isOwnerOf(object $obj) : bool + public function isOwnerOf(obj $obj) : bool { return $obj->getOwner()->guid == $this->guid; } @@ -43,7 +43,7 @@ abstract class agent extends object /* * Check whether this agent is a member of the given object */ - public function isMemberOf(object $obj) : bool + public function isMemberOf(obj $obj) : bool { foreach ($obj->getMembers() as $memb) { @@ -114,7 +114,7 @@ abstract class agent extends object * Check whether this agent has access permission for given * object */ - public function canAccess(object $obj) : bool + public function canAccess(obj $obj) : bool { if ($this->admin) return true; @@ -130,13 +130,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canAccessSub($parent)) return true; } else if ($this->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canAccessSub($owner)) return true; } @@ -148,7 +148,7 @@ abstract class agent extends object * Check whether this agent has modify permission for given * object */ - public function canModify(object $obj) : bool + public function canModify(obj $obj) : bool { if ($this->admin) return true; @@ -161,13 +161,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySub($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySub($owner)) return true; } @@ -179,7 +179,7 @@ abstract class agent extends object * Check whether this agent has modify members permission for * given object */ - public function canModifyMembers(object $obj) : bool + public function canModifyMembers(obj $obj) : bool { if ($this->admin) return true; @@ -192,13 +192,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySubMembers($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySubMembers($owner)) return true; } @@ -210,7 +210,7 @@ abstract class agent extends object * Check whether this agent has modify permissions permission * for given object */ - public function canModifyPermissions(object $obj) : bool + public function canModifyPermissions(obj $obj) : bool { if ($this->admin) return true; @@ -220,13 +220,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySubPermissions($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySubPermissions($owner)) return true; } @@ -238,7 +238,7 @@ abstract class agent extends object * Check whether this agent has access-sub permission for * given object */ - public function canAccessSub(object $obj) : bool + public function canAccessSub(obj $obj) : bool { if ($this->admin) return true; @@ -254,13 +254,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canAccessSub($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canAccessSub($owner)) return true; } @@ -272,7 +272,7 @@ abstract class agent extends object * Check whether this agent has create-sub permission * for given object */ - public function canCreateSub(object $obj) : bool + public function canCreateSub(obj $obj) : bool { if ($this->admin) return true; @@ -288,13 +288,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canCreateSub($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canCreateSub($owner)) return true; } @@ -306,7 +306,7 @@ abstract class agent extends object * Check whether this agent has modify-sub permission * for given object */ - public function canModifySub(object $obj) : bool + public function canModifySub(obj $obj) : bool { if ($this->admin) return true; @@ -319,13 +319,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySub($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySub($owner)) return true; } @@ -337,7 +337,7 @@ abstract class agent extends object * Check whether this agent has modify-sub-members * permission for given object */ - public function canModifySubMembers(object $obj) : bool + public function canModifySubMembers(obj $obj) : bool { if ($this->admin) return true; @@ -350,13 +350,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySubMembers($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySubMembers($owner)) return true; } @@ -368,7 +368,7 @@ abstract class agent extends object * Check whether this agent has modify-sub-permissions * permission for given object */ - public function canModifySubPermissions(object $obj) : bool + public function canModifySubPermissions(obj $obj) : bool { if ($this->admin) return true; @@ -378,13 +378,13 @@ abstract class agent extends object if ($obj->parent) { - $parent = new object($obj->parent); + $parent = new obj($obj->parent); if ($this->canModifySubPermissions($parent)) return true; } else if ($obj->owner) { - $owner = new object($obj->owner); + $owner = new obj($obj->owner); if ($this->canModifySubPermissions($owner)) return true; } diff --git a/app/class/issue.class.php b/app/class/issue.class.php index b61a6e3..1c77894 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -12,7 +12,7 @@ * For more information, please refer to UNLICENSE */ -require_once "class/object.class.php"; +require_once "class/obj.class.php"; require_once "class/stage.class.php"; require_once "class/user.class.php"; require_once "class/mesg.class.php"; @@ -21,7 +21,7 @@ require_once "class/mesg.class.php"; * This class models Scrott issues. Issues represent units of work, can * be assigned to users, and advance through a pipeline. */ -class issue extends object +class issue extends obj { /* * Constructor diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index 2512d03..1a864c0 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -12,7 +12,7 @@ * For more information, please refer to UNLICENSE */ -require_once "class/object.class.php"; +require_once "class/obj.class.php"; require_once "class/user.class.php"; require_once "class/pad.class.php"; require_once "class/stage.class.php"; @@ -22,7 +22,7 @@ require_once "class/issue.class.php"; * This class models issue activity, private messaging, pad discussions, * and system and object log messages. */ -class mesg extends object +class mesg extends obj { /* * Constants used for uploading attachments @@ -56,7 +56,7 @@ class mesg extends object /* * Initialize a new regular message. */ - public static function initNew(string $message, user $author, object $parent) : mesg + public static function initNew(string $message, user $author, obj $parent) : mesg { $mesg = new mesg(); $mesg->setOwner($author); @@ -106,7 +106,7 @@ class mesg extends object /* * Initialize a new log message. */ - public static function initNewLog(string $message, user $author, object $parent) : mesg + public static function initNewLog(string $message, user $author, obj $parent) : mesg { $owner = $parent->getOwner(); @@ -321,7 +321,7 @@ class mesg extends object break; } - $rcpt = object::arrayUnique($rcpt); + $rcpt = obj::arrayUnique($rcpt); $author = $this->author; $rcpt = array_filter($rcpt, function ($val) use($author) { return $val->guid != $author; }); $attachPath = ($this->getAttachment() ? "dynmic/attach/" . $this->guid : NULL); diff --git a/app/class/pad.class.php b/app/class/pad.class.php index 3966f58..38812d6 100644 --- a/app/class/pad.class.php +++ b/app/class/pad.class.php @@ -12,7 +12,7 @@ * For more information, please refer to UNLICENSE */ -require_once "class/object.class.php"; +require_once "class/obj.class.php"; require_once "class/agent.class.php"; require_once "class/stage.class.php"; @@ -20,7 +20,7 @@ require_once "class/stage.class.php"; * This class models Scrott pads. Pads are the space for projects to track * issues and communicate. */ -class pad extends object +class pad extends obj { /* * Constructor diff --git a/app/class/stage.class.php b/app/class/stage.class.php index 74c5f42..43bb3c3 100644 --- a/app/class/stage.class.php +++ b/app/class/stage.class.php @@ -12,7 +12,7 @@ * For more information, please refer to UNLICENSE */ -require_once "class/object.class.php"; +require_once "class/obj.class.php"; require_once "class/pad.class.php"; require_once "class/issue.class.php"; @@ -20,7 +20,7 @@ require_once "class/issue.class.php"; * This class models Scrott pad stages. Stages form a pipeline through * which issues can progress. */ -class stage extends object +class stage extends obj { /* * Constructor @@ -173,7 +173,7 @@ class stage extends object * moved to the given stage object. Additionally, the pad may be * given, in which case, those issues will be closed. */ - public function removeStage(object $mvt) : void + public function removeStage(obj $mvt) : void { if (!($prev = $this->getPrev())) $prev = $this->getParent(); -- cgit v1.2.3 From 98ca92aa0a8aa9d879dd77ab76672f052b53b8d6 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 9 Feb 2018 01:06:26 -0500 Subject: Fix bug in function user::getCurrent() If the session is set to an invalid (eg: deleted) user GUID, an exception is (correctly) thrown. This commit catches that and enables getCurrent() to close the bad session and return NULL. --- app/class/user.class.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 81fc29f..6f05570 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -146,7 +146,17 @@ class user extends agent return NULL; } - return new user($_SESSION['userguid']); + try + { + return new user($_SESSION['userguid']); + } + catch (Exception $e) + { + /* invalid user */ + self::setCurrent(); + location("/"); + return NULL; + } } /* -- cgit v1.2.3 From f1c92c8a67fee9d30480c6a72ac3d00b1879edfd Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 11 Feb 2018 16:15:36 -0500 Subject: Address issue with user functions getCurrent() and setCurrent() Previously, these functions would always call session_start() before doing most of their work. However, I've found that calling that function two or more times within the lifetime of a program results in NOTICE messages output from the PHP interpreter. Therefore, I am now only calling session_start() if the session is not already active. --- app/class/user.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 6f05570..50679ee 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -131,7 +131,7 @@ class user extends agent */ public static function getCurrent() : ?user { - if (!session_start()) + if ((session_status() != PHP_SESSION_ACTIVE) && !session_start()) throw new Exception("Unable to aquire a PHP session"); if (!isset($_SESSION['userguid'])) @@ -168,7 +168,7 @@ class user extends agent */ public static function setCurrent(?user $user = NULL) : void { - if (!session_start()) + if ((session_status() != PHP_SESSION_ACTIVE) && !session_start()) throw new Exception("Unable to aquire a PHP session"); unset($_SESSION['userguid']); -- cgit v1.2.3 From 41edfa2f7c85d657ff41ba146bd45de84373281c Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 21 Jul 2018 23:03:15 -0400 Subject: Add PAGE_OBJECT global mechanism This addresses a problem with most views. They need an object context to display in. IE what pad, group, etc. are we viewing? This variable is intended to be set by index.php and referenced by page models. --- app/class/globals.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 776fc35..8d27ae4 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -12,6 +12,8 @@ * For more information, please refer to UNLICENSE */ +require_once "class/obj.class.php"; + /* * This file defines various functions which exist in the global namespace. * These are utility functions and constants for the Scrott application. @@ -32,6 +34,14 @@ $_SCROTT[ERROR] = array(); $_SCROTT[WARNING] = array(); $_SCROTT[NOTICE] = array(); +/* + * The page object is the object our current request is primarily + * interested in. For example, when navigating to a pad web page by + * its guid, that pad is the page object. When viewing the dashboard, + * the current logged-in user is the page object. + */ +$_SCROTT['PAGE_OBJECT'] = NULL; + /* * Get the application root path. This is an absolute path on the server. */ @@ -125,6 +135,29 @@ function getErrors(string $level) : array return $_SCROTT[$level]; } +/* + * Set the page object for the current request. This can only + * be called once per runtime. + */ +function setPageObj(obj $obj) : void +{ + global $_SCROTT; + + if ($_SCROTT['PAGE_OBJECT'] !== NULL) + throw new Exception("Tried to establish page object context twice"); + + $_SCROTT['PAGE_OBJECT'] = $obj; +} + +/* + * Get the page object for the current request. + */ +function getPageObj() : ?obj +{ + global $_SCROTT; + return $_SCROTT['PAGE_OBJECT']; +} + /* * Save an uploaded file and impose some constraints on supplied * data. Caller can optionally pass some strings by reference to -- cgit v1.2.3 From 72ba54e1224ef815f8f7dabdf0c7449b120cfc4c Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 21 Jul 2018 23:10:05 -0400 Subject: Change errorlevel constants --- app/class/globals.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 8d27ae4..ab82edd 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -26,9 +26,9 @@ define("__VERSION__", "v0.0"); * logic to report errors, warnings, or informational responses to the * user in cases where an exception doesn't need to be thrown. */ -define("ERROR", "errorlist"); -define("WARNING", "warninglist"); -define("NOTICE", "noticelist"); +define("ERROR", "ERROR_LIST"); +define("WARNING", "WARNING_LIST"); +define("NOTICE", "NOTICE_LIST"); $_SCROTT[ERROR] = array(); $_SCROTT[WARNING] = array(); -- cgit v1.2.3 From 3eaf6f3b990d86069453b1bda4ed377e67ccf571 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 22 Jul 2018 02:25:54 -0400 Subject: Fix bug in function agent::isOwner() If the argument doesn't have an owner, then an access error is thrown when we try to do ->guid. Since there is no owner, just return false. Otherwise, do the comparision as usual. --- app/class/agent.class.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 6d0e20d..b4e6702 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -37,7 +37,10 @@ abstract class agent extends obj */ public function isOwnerOf(obj $obj) : bool { - return $obj->getOwner()->guid == $this->guid; + if (!($own = $obj->getOwner())) + return false; + + return $own->guid == $this->guid; } /* -- cgit v1.2.3 From 6c03cc537c5794a131278583f83477bbd15e0e3e Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 22 Jul 2018 02:43:02 -0400 Subject: Fix bug in agent 'has permission' functions The check that this commit adds to each of these functions enables users with all permissions on themselves. --- app/class/agent.class.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index b4e6702..4af13d5 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -122,6 +122,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -156,6 +159,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -187,6 +193,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -218,6 +227,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -246,6 +258,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -280,6 +295,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -314,6 +332,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -345,6 +366,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; @@ -376,6 +400,9 @@ abstract class agent extends obj if ($this->admin) return true; + if ($this->guid == $obj->guid) + return true; + if ($this->isOwnerOf($obj)) return true; -- cgit v1.2.3 From 380187ee5aadf008c7a71a9015008c155c528ccc Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 22 Jul 2018 03:31:24 -0400 Subject: Update function location() Passing no argument (or NULL) now causes this function to redirect to the 'app-path' (current request page). This is a way to reload the current page. --- app/class/globals.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index ab82edd..c81c9a2 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -69,11 +69,14 @@ function redirect(string $url) : void /* * Redirect to the given in-app URL and die. The given URL should be a path - * relative to the app root. + * relative to the app root. No argument reloads the current path. */ -function location(string $path) : void +function location(?string $path = NULL) : void { - redirect(ar() . $path); + if ($path) + redirect(ar() . $path); + else + redirect(ap()); } /* -- cgit v1.2.3 From fc45eabca659f9e9792ca4c5a82a3cef732cec10 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Tue, 24 Jul 2018 04:59:34 -0400 Subject: Add $_SCROTT['PAGE_NAME'] variable and getter/setter The intention is for index.php to set this variable. This is the text (HTML) displayed on the button for the pad select dropdown in the nav bar. Basically the canonical name of the page we're on. --- app/class/globals.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index c81c9a2..7120aaa 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -42,6 +42,13 @@ $_SCROTT[NOTICE] = array(); */ $_SCROTT['PAGE_OBJECT'] = NULL; +/* + * The page name variable is what displays in the pad select dropdown and + * is typically set to the "group / pad" of the current page or, "Dashboard", + * "Groups", "Pads", etc. on special pages. + */ +$_SCROTT['PAGE_NAME'] = ""; + /* * Get the application root path. This is an absolute path on the server. */ @@ -161,6 +168,29 @@ function getPageObj() : ?obj return $_SCROTT['PAGE_OBJECT']; } +/* + * Set the page name string. This can only be called once per + * runtime. + */ +function setPageName(string $name) : void +{ + global $_SCROTT; + + if ($_SCROTT['PAGE_NAME'] !== "") + throw new Exception("Tried to configure page name twice"); + + $_SCROTT['PAGE_NAME'] = $name; +} + +/* + * Get the page name string. + */ +function getPageName() : string +{ + global $_SCROTT; + return $_SCROTT['PAGE_NAME']; +} + /* * Save an uploaded file and impose some constraints on supplied * data. Caller can optionally pass some strings by reference to -- cgit v1.2.3 From 84b7c560ffc09a65df896116ccd63b2f132b92b0 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 26 Jul 2018 03:52:21 -0400 Subject: Update function obj::getMembers() Added a $limit argument to specify a maximum number of results to return. --- app/class/obj.class.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/obj.class.php b/app/class/obj.class.php index 5be9ac3..4e44649 100644 --- a/app/class/obj.class.php +++ b/app/class/obj.class.php @@ -144,12 +144,17 @@ class obj extends table } /* - * Get an array of all members of this object + * Get an array of all members of this object. Limit of zero + * returns all members. */ - public function getMembers() : array + public function getMembers(int $limit = 0) : array { $memb = array(); $query = "SELECT member FROM members WHERE guid = '" . database::esc($this->guid) . "'"; + + if ($limit != 0) + $query .= " LIMIT " . database::esc($limit); + $res = database::query($query); foreach ($res as $m) -- cgit v1.2.3 From 575d532b06eeae5eac77f231265e8fd7034d4fd7 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 7 Sep 2018 07:02:18 -0400 Subject: Fix bug in function table->loadObj() If a table query yeilds zero rows, we would still attempt to load the first (index zero) into $this, causing an error to be thrown by PHP. We are now checking the size of the results array first. --- app/class/table.class.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 52a3e7d..28308bb 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -57,7 +57,13 @@ abstract class table { $tbl = database::esc($tbl); $query = "SELECT * FROM " . $tbl . " WHERE guid = '" . $guid . "'"; - $res = database::query($query)[0]; + $res = database::query($query); + + /* no results */ + if (count($res) == 0) + continue; + + $res = $res[0]; foreach ($flds as $fld) { -- cgit v1.2.3 From 0a77d66a62006fce5e3768eb115baa48b16cb719 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 7 Sep 2018 07:08:26 -0400 Subject: Fix typo --- app/class/user.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/user.class.php b/app/class/user.class.php index 50679ee..90aac44 100644 --- a/app/class/user.class.php +++ b/app/class/user.class.php @@ -267,7 +267,7 @@ class user extends agent /* * Get all groups this user owns or is a member of. This isn't necessarily - * all groups this user cas access permissions for. Results are sorted by + * all groups this user has access permissions for. Results are sorted by * ownership, then by name. */ public function getGroups_ordByOwnByName() : array -- cgit v1.2.3 From 11f83706e5774b417f6d8a52786e925bec88fc43 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 12 Sep 2018 02:53:53 -0400 Subject: Add function agent::getAgentObj() This is basically a constructor for agent. The actual type returned is a contrete agent. --- app/class/agent.class.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 4af13d5..63a21ed 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -32,6 +32,23 @@ abstract class agent extends obj parent::__construct($guid); } + /* + * Since this class is abstract, this function is provided as a + * means of constructing the appropriate agent object based on + * a GUID. + */ + public static function getAgentObj(string $guid) : agent + { + try + { + return new user($guid); + } + catch (Exception $e) + { + return new group($guid); + } + } + /* * Check whether this agent is the owner of the given object */ -- cgit v1.2.3 From 323576a594f20485e25c55dd52a5356cb209479c Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 15 Sep 2018 21:51:23 -0400 Subject: Add function oneStr() The initial intended use case for this is applying the "active" and "in active" classes to the first tab to appear in the settings modal. --- app/class/globals.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 7120aaa..8ac67e1 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -191,6 +191,21 @@ function getPageName() : string return $_SCROTT['PAGE_NAME']; } +/* + * Produce a string, but only once. This function is useful + * when dealing with some variable collection of markup and + * you want to affect only the first one with a modifier. + * $state should be initialized to false prior to first call. + */ +function oneStr(string $str, bool &$state) : string +{ + if ($state) + return ""; + + $state = true; + return $str; +} + /* * Save an uploaded file and impose some constraints on supplied * data. Caller can optionally pass some strings by reference to -- cgit v1.2.3 From 6b49c22dde73d3770f0f49f075ec86ff28919674 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 16 Sep 2018 16:00:19 -0400 Subject: Add function obj::hasHeadImg() We can check for the existence of an object's background image by calling getBgImg(), since it returns NULL when there is no such image. But getHeadImg() behaves differently, returning a path to 'static/img/null.jpg' (via df.php) when there is no image, making it more difficult to tell. This function addresses this concern. --- app/class/obj.class.php | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'app/class') diff --git a/app/class/obj.class.php b/app/class/obj.class.php index 4e44649..003738b 100644 --- a/app/class/obj.class.php +++ b/app/class/obj.class.php @@ -211,6 +211,16 @@ class obj extends table return $mesgs; } + /* + * Check whether object has a custom head set. This is necessary, since + * getHeadImg() will return a path to a default if the object doesn't + * have its own. + */ + public function hasHeadImg() : bool + { + return is_file("dynmic/heads/" . $this->guid); + } + /* * Get the URL to the head image resource for this object */ -- cgit v1.2.3 From 77cbdc84d952643163d7086f79a633e5837be76d Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Sep 2018 15:30:44 -0400 Subject: globals: Add function saveIfFile() This is an alternative function to globals' saveFile(), which allows model code to just pass in the name of the expected uploaded file, rather than requiring them to look up the file themselves. This is in line with my preference to encapsulate PHP superglobals access away from most of the codebase. Note that even if the user opts not to upload optional files, the associated file field will still be present in $_FILES, with a special error code set (meaning 'no file uploaded') which setFile() ignores. It is only in the case of a malformed form submission that $_FILES will be missing the requested file field, prompting Scrott to throw an exception. --- app/class/globals.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 8ac67e1..6e3b7fd 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -247,4 +247,19 @@ function saveFile(array $file, string $path, int $maxsize, ?array $allowedMime = return true; } +/* + * Similar to saveFile, but takes the uploaded file field name, + * rather than the array directly. The file is looked up in + * $_FILES. If it does not exist, an exception is thrown. + */ +function saveIfFile(string $file, string $path, int $maxsize, ?array $allowedMime = NULL, + ?string &$origName = NULL, ?string &$origMime = NULL) : bool +{ + if (!isset($_FILES[$file])) + throw new Exception("Requested file upload, but no data was supplied"); + + $f = $_FILES[$file]; + return saveFile($f, $path, $maxsize, $allowedMime, $origName, $origMime); +} + ?> -- cgit v1.2.3 From 866c16abdce264362edb7f5a3c35e7bab9ddf2a5 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Sep 2018 15:46:26 -0400 Subject: Update all usage of saveFile() Update all usage of saveFile() to use added saveIfFile() function, forwarding on the convenience to model code. Model code can pass in file field names, rather than $_FILES arrays directly. --- app/class/mesg.class.php | 4 ++-- app/class/obj.class.php | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index 1a864c0..2e5c9d6 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -246,12 +246,12 @@ class mesg extends obj * during new message creation and not changed afterward. Returns * false if there is a problem saving the attachment. */ - public function setAttachment(array $file) : bool + public function setAttachment(string $file) : bool { $path = "dynmic/attach/" . $this->guid; $origName = ""; - $ret = saveFile($file, $path, self::ATTACH_MAXSIZE, NULL, $origName); + $ret = saveIfFile($file, $path, self::ATTACH_MAXSIZE, NULL, $origName); $this->attachment = $origName; $this->saveObj(); diff --git a/app/class/obj.class.php b/app/class/obj.class.php index 003738b..199529b 100644 --- a/app/class/obj.class.php +++ b/app/class/obj.class.php @@ -231,14 +231,13 @@ class obj extends table /* * Set the head image for this object, overwriting any existing - * image. $image should be an uploaded file to PHP, still - * unhandled. + * image. $image should be the name of the file formctrl field. */ - public function setHeadImg(array $image) : bool + public function setHeadImg(string $image) : bool { $path = "dynmic/heads/" . $this->guid; - if (!saveFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) + if (!saveIfFile($image, $path, self::HEAD_MAXSIZE, self::IMAGE_MIME)) return false; if (!imageSquareCrop($path)) @@ -276,13 +275,13 @@ class obj extends table /* * Set the background image for this object, overwriting any - * existing image. $image should be an uploaded file to PHP, - * still unhandled. + * existing image. $image should be the name of the file + * formctrl field. */ - public function setBgImg(array $image) : bool + public function setBgImg(string $image) : bool { $path = "dynmic/bgs/" . $this->guid; - return saveFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); + return saveIfFile($image, $path, self::BG_MAXSIZE, self::IMAGE_MIME); } /* -- cgit v1.2.3 From eef89807b15b0e38362d67b68a5becacc8838864 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Sep 2018 16:39:31 -0400 Subject: globals: Remove single call assertions for setPage...() functions Removing these unnecessary checks. They are not protecting us from anything, only inconveniencing me in index.php. --- app/class/globals.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index 6e3b7fd..c1a6e32 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -146,16 +146,11 @@ function getErrors(string $level) : array } /* - * Set the page object for the current request. This can only - * be called once per runtime. + * Set the page object for the current request. */ function setPageObj(obj $obj) : void { global $_SCROTT; - - if ($_SCROTT['PAGE_OBJECT'] !== NULL) - throw new Exception("Tried to establish page object context twice"); - $_SCROTT['PAGE_OBJECT'] = $obj; } @@ -169,16 +164,11 @@ function getPageObj() : ?obj } /* - * Set the page name string. This can only be called once per - * runtime. + * Set the page name string. */ function setPageName(string $name) : void { global $_SCROTT; - - if ($_SCROTT['PAGE_NAME'] !== "") - throw new Exception("Tried to configure page name twice"); - $_SCROTT['PAGE_NAME'] = $name; } -- cgit v1.2.3 From e90ec83ac5f9327dcf055174002723b48948a679 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 19 Sep 2018 17:49:34 -0400 Subject: table: Change function visibilities to public These two functions, 'getCurrentTimestamp()' and 'isGUID()' are updated to be public. There is actually no good reason for them to be private; I originally just never antisipated their use outside this class. I need isGUID() in index.php to help with page routing. Neither of these two functions have side effects of any kind nor any unexpected behavior, so there is no harm in going public. --- app/class/table.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 28308bb..5e4c823 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -191,7 +191,7 @@ abstract class table /* * Get current timestamp as a string for object database purposes */ - private static function getCurrentTimestamp() : string + public static function getCurrentTimestamp() : string { $query = "SELECT now() AS stamp"; $res = database::query($query); @@ -201,7 +201,7 @@ abstract class table /* * Check whether the given GUID exists */ - private static function isGUID(string $guid) : bool + public static function isGUID(string $guid) : bool { $guid = database::esc($guid); $query = "SELECT guid FROM objects WHERE guid = '" . $guid . "'"; -- cgit v1.2.3 From 1daec62c0ab6590d3a30464b2f679baea3ca3936 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 20 Sep 2018 00:09:08 -0400 Subject: table: Fix bug in constructor This particular flaw was dampening (and could popentially be hiding) the effects of other bugs. For instance, in this case, a GUID of "" was invalidly being used to construct an object. This should obviously be considered an error, but since "" evaluates to false, the construction was treated as default (no GUID) construction and succedded. It wasn't until later when missing properties were accessed that random PHP error messages clued me into what was happening. Now, when any sort of explicit value is used to construct an object (not NULL), an object load will be attempted, giving bad input more chances to fail outright and trigger an exception. In addition, the 'no such guid' exception message is updated to place quotes ('') around the GUID string to make it more obvious when "" is used in the future. --- app/class/table.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/table.class.php b/app/class/table.class.php index 5e4c823..618d938 100644 --- a/app/class/table.class.php +++ b/app/class/table.class.php @@ -37,7 +37,7 @@ abstract class table */ public function __construct(?string $guid = NULL) { - if ($guid) + if ($guid !== NULL) $this->loadObj($guid); } @@ -51,7 +51,7 @@ abstract class table $guid = database::esc($guid); if (!self::isGUID($guid)) - throw new Exception("GUID " . $guid . " does not exist"); + throw new Exception("GUID '" . $guid . "' does not exist"); foreach ($this->fields as $tbl => $flds) { -- cgit v1.2.3 From d16ddc164b82c594680359841eeeaa1e26aceaa1 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Thu, 20 Sep 2018 00:28:17 -0400 Subject: pad: Fix bug in function getStages() In cases where the pad had no stages beneath it, `new stage($this->stage)` would construct an invalid object. As it turns out, calling ->getArray() on an uninitialized stage object yeilds bad results. Instead of patching the stage::getArray() function, I add a check to harden pad::getStages(). My reasoning for this is as follows: The bug in getArray() manifests from a domain error, ie. it's only because we are calling it on an uninitialized object. The object is already in a bad state prior to caling getArray(). Rather, I opt to patch getStages() so that we never create a bad object in the first place. Now, for no-stage pads, getStages() will return early an empty array. --- app/class/pad.class.php | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app/class') diff --git a/app/class/pad.class.php b/app/class/pad.class.php index 38812d6..dcf2b32 100644 --- a/app/class/pad.class.php +++ b/app/class/pad.class.php @@ -91,6 +91,9 @@ class pad extends obj */ public function getStages() : array { + if (!isset($this->stage) || $this->stage == "") + return array(); + $stage = new stage($this->stage); return $stage->getArray(); } -- cgit v1.2.3 From 652a62ede6849d04302153d8a3a7fad492b18c5b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Fri, 21 Sep 2018 22:49:15 -0400 Subject: settings: Add key 'smtpResult' This is not meant to be a user (admin) configurable key. Rather, this is a mechanism for success/failure results from interactions with PHPMailer to make their way back to the UI. Down the road, email sending functions should publish their true/false return value to this configuration key. The initial default value of "NULL" means no mail send attempts have taken place. --- app/class/settings.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/settings.class.php b/app/class/settings.class.php index 19fba6a..d936a62 100644 --- a/app/class/settings.class.php +++ b/app/class/settings.class.php @@ -147,6 +147,14 @@ abstract class settings { return self::option("smtpPasswd", "", $value); } + + /* + * SMTP result of last attempted send - 'true', 'false', 'NULL' + */ + public static function smtpResult(?string $value = NULL) : string + { + return self::option("smtpResult", "NULL", $value); + } } ?> -- cgit v1.2.3 From bc897063c822ee90fb23abf5189cc2b95e1a4f76 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 22 Sep 2018 03:15:58 -0400 Subject: database: Fix bug in function checkConfig() Because of how this function was implemented, any failure during database instance construction is treated the same way. IE. we cannot tell the difference between 'no db config' (as is the initial default state) and a 'bad db config' (either bogus data, or the server happens to be down). Because of this, if, after the database access is initially set up, access to the db becomes unavailable or someone makes a bad edit to the dbconfig.php file, Scrott behaves as if it is being configured for the first time. This is *dangerous* behavior! (unexpected, at the least) The implication of this is that if Scrott's database access is ever incidentially interrupted, the very next visitor to the site is offered the chance to (silently) reconfigure the server to point to any database of his choosing. This patch updates the checkConfig() function to only 'soft fail' (return false) in the case where the configuration is _actually_ missing. IE. $_SCROTT['conf'] is not defined. This function will otherwise passthrough any and all exceptions which result from instanciating the database instance and will only return true if both of these steps succeed. --- app/class/database.class.php | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'app/class') diff --git a/app/class/database.class.php b/app/class/database.class.php index 3d94e16..a2cab42 100644 --- a/app/class/database.class.php +++ b/app/class/database.class.php @@ -104,15 +104,12 @@ abstract class database */ public static function checkConfig() : bool { - try - { - $db = self::getInstance(); - } - catch (Exception $e) - { + global $_SCROTT; + + if (!isset($_SCROTT['conf'])) return false; - } + $db = self::getInstance(); return true; } -- cgit v1.2.3 From fd58becfca433f125df70531f9bece88c8f14b71 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 23 Sep 2018 22:05:39 -0400 Subject: form: Fix bug in populate() There was a problem with processing enum type fields. The way all other field types are asserted to be 'defined' is via: isset($field) && $field != "" Which works perfectly fine, and is exactly what we want. However, with enums the second part of that && can bite us if "" is in the list of acceptable values. This commit removed that half of the check (only for enum values) so that the empty string may be an acceptable enum value. If "" is not in the values array, then the check is implicitly reinstated. --- app/class/form.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/form.class.php b/app/class/form.class.php index 8f9d936..10a68c3 100644 --- a/app/class/form.class.php +++ b/app/class/form.class.php @@ -154,7 +154,7 @@ class form /* init enum fields */ foreach ($this->enumFields as $field) { - if (isset($input[$field['name']]) && $input[$field['name']] != "") + if (isset($input[$field['name']])) { if (array_search($input[$field['name']], $field['values']) === false) { -- cgit v1.2.3 From fbb9bcb787287597a0666b9313a4754ed03d242b Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Mon, 24 Sep 2018 12:55:21 -0400 Subject: agent: Fix bug in function canAccess() This is probabally more of an oops than a bug, although was causing unexpected behavior. When falling back to checking whether the agent has access to the object's owner, it was wrongly accessing through $this->owner, rather than $obj->owner (which is the function argument). This was probabally left over from how this function _used_ to be implemented (you would call on the object and pass in the user). --- app/class/agent.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index 63a21ed..c8e6436 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -157,7 +157,7 @@ abstract class agent extends obj if ($this->canAccessSub($parent)) return true; } - else if ($this->owner) + else if ($obj->owner) { $owner = new obj($obj->owner); if ($this->canAccessSub($owner)) -- cgit v1.2.3 From 9df5344050ec0a2b8bec03c7a89fff9d7d41ce2f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 20 Oct 2018 21:24:01 -0400 Subject: issue: Add author and authored fields --- app/class/issue.class.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 1c77894..651096e 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -32,8 +32,10 @@ class issue extends obj "guid", "numb", "assignee", + "author", "seen", "description", + "authored", "due", "tags", ); @@ -58,6 +60,9 @@ class issue extends obj $issue->name = $name; $issue->objtype = "issue"; $issue->numb = $numb; + $issue->setAuthor($owner); + $issue->saveObj(); // get timestamp + $issue->authored = $issue->created; $issue->saveObj(); return $issue; } @@ -83,6 +88,30 @@ class issue extends obj $this->saveObj(); } + /* + * Get the author of this issue. This is usually the user + * that opened the issue, but may differ if this issue was + * elevated from a previous discussion thread. + */ + public function getAuthor() : user + { + if (!isset($this->author) || $this->author == "") + return NULL; + + return new user($this->author); + } + + /* + * Set the author of this issue. This should usually only + * be done while constructing a new message or to clear out + * references to a user that got removed. + */ + public function setAuthor(user $author) : void + { + $this->author = $author->guid; + $this->saveObj(); + } + /* * Get the pad this issue exists under */ -- cgit v1.2.3 From b093b3affe3ac6878e2242bff310dc466687a825 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 20 Oct 2018 21:43:46 -0400 Subject: issue: Add open/close data --- app/class/issue.class.php | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 651096e..5d1daec 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -33,9 +33,12 @@ class issue extends obj "numb", "assignee", "author", + "closer", "seen", "description", + "opened", "authored", + "closed", "due", "tags", ); @@ -62,6 +65,7 @@ class issue extends obj $issue->numb = $numb; $issue->setAuthor($owner); $issue->saveObj(); // get timestamp + $issue->opened = $issue->created; $issue->authored = $issue->created; $issue->saveObj(); return $issue; @@ -112,6 +116,27 @@ class issue extends obj $this->saveObj(); } + /* + * Get the user that closed this issue. If the issue is still + * open, NULL is returned. + */ + public function getCloser() : ?user + { + if (!isset($this->closer) || $this->closer == "") + return NULL; + + return new user($this->closer); + } + + /* + * Mark the user that closed this issue. + */ + public function setCloser(user $closer) : void + { + $this->closer = $closer->guid; + $this->saveObj(); + } + /* * Get the pad this issue exists under */ @@ -145,12 +170,16 @@ class issue extends obj /* * Mark this issue as completed and closed. */ - public function close() : void + public function close(user $closer) : void { $pad = $this->getParent()->getParent(); if ($pad) + { + $this->closed = self::getCurrentTimestamp(); + $this->setCloser($closer); $this->setParent($pad); + } } } -- cgit v1.2.3 From b690505b0e1e255e5081adcf49c724186bb831c2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 20 Oct 2018 21:49:32 -0400 Subject: issue: Add assigned timestamp --- app/class/issue.class.php | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 5d1daec..57fc588 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -37,6 +37,7 @@ class issue extends obj "seen", "description", "opened", + "assigned", "authored", "closed", "due", @@ -89,6 +90,7 @@ class issue extends obj { $this->seen = 0; $this->assignee = $assignee->guid; + $this->assigned = self::getCurrentTimestamp(); $this->saveObj(); } -- cgit v1.2.3 From 792e741899cc895651e7b12a38b688c1da6406db Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 20 Oct 2018 21:55:53 -0400 Subject: issue: Add function isOpen() --- app/class/issue.class.php | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 57fc588..0fc12e4 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -183,6 +183,14 @@ class issue extends obj $this->setParent($pad); } } + + /* + * Check whether issue is currently open or closed. + */ + public function isOpen() : bool + { + return self::typeOf($this->parent) != "pad"; + } } ?> -- cgit v1.2.3 From 62872702dc413b7abab94d8a5a7bd21770b5d241 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 20 Oct 2018 22:14:44 -0400 Subject: mesg: Update function makeIssue() This function is patched to co-operate with structural changes to the issue class. --- app/class/mesg.class.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index 2e5c9d6..d40028c 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -266,13 +266,14 @@ class mesg extends obj * object is created and this message object will be destroyed. If * this is not an eligible message for promotion, NULL is returned. */ - public function makeIssue(stage $parent) : ?issue + public function makeIssue(user $owner, stage $parent) : ?issue { if ($this->getParent()->objtype != "pad") return NULL; - $issue = issue::initNew($this->name, $this->getOwner(), $parent); - $issue->created = $this->created; + $issue = issue::initNew($this->name, $owner, $parent); + $issue->author = $this->author; + $issue->authored = $this->created; $issue->description = $this->mesg; $issue->saveObj(); -- cgit v1.2.3 From a18205b9991f9e5d6ac0a86aa37f049bf53980f6 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 21 Oct 2018 21:59:55 -0400 Subject: issue: Rewrite issue class Revised implementation of redesigned data model. --- app/class/issue.class.php | 179 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 125 insertions(+), 54 deletions(-) (limited to 'app/class') diff --git a/app/class/issue.class.php b/app/class/issue.class.php index 0fc12e4..47adfa1 100644 --- a/app/class/issue.class.php +++ b/app/class/issue.class.php @@ -18,8 +18,8 @@ require_once "class/user.class.php"; require_once "class/mesg.class.php"; /* - * This class models Scrott issues. Issues represent units of work, can - * be assigned to users, and advance through a pipeline. + * This class models Scrott issues. Issues represent units of work, track + * messages, can be assigned to users, and advance through a pipeline. */ class issue extends obj { @@ -31,14 +31,8 @@ class issue extends obj $this->fields['issues'] = array( "guid", "numb", - "assignee", - "author", + "mesg", "closer", - "seen", - "description", - "opened", - "assigned", - "authored", "closed", "due", "tags", @@ -49,10 +43,11 @@ class issue extends obj } /* - * Initialize a new issue object with the given name, parent, and - * owner. + * Initialize a new issue object with the given message, parent, + * and owner. The issue's name is taken from the message's name. + * The given message object is updated to parent the new issue. */ - public static function initNew(string $name, user $owner, stage $parent) : issue + public static function initNew(mesg $mesg, user $owner, stage $parent) : issue { $pad = $parent->getParent(); $numb = $pad->issueNumb++; @@ -61,61 +56,146 @@ class issue extends obj $issue = new issue(); $issue->setOwner($owner); $issue->setParent($parent); - $issue->name = $name; + $issue->name = $mesg->name; $issue->objtype = "issue"; $issue->numb = $numb; - $issue->setAuthor($owner); - $issue->saveObj(); // get timestamp - $issue->opened = $issue->created; - $issue->authored = $issue->created; + $issue->mesg = $mesg->guid; $issue->saveObj(); + + $mesg->setParent($issue); + return $issue; } /* - * Get the assignee for this issue + * Get all assignees. Assignees are returned as an array of + * stdClass instances. Objects contain data from the 'assignees' + * database table with no defined operations. Pointers to users + * will be followed and an instanciated user object will be + * in their place. Limit of zero returns all assignees. */ - public function getAssignee() : ?user + public function getAssignees(int $limit = 0) : array { - if (!isset($this->assignee) || $this->assignee == "") - return NULL; + $assign = array(); + $query = "SELECT * FROM assignees WHERE guid = '" . + database::esc($this->guid) . "'"; + + if ($limit != 0) + $query .= " LIMIT " . database::esc($limit); + + $res = database::query($query); + + foreach ($res as $a) + { + $obj = new stdClass(); + + foreach ($a as $k => $v) + $obj->{$k} = $v; + + $obj->assignee = new user($obj->assignee); + $obj->assigner = new user($obj->assigner); + + if (isset($obj->dismisser) && $obj->dismisser != "") + $obj->dismisser = new user($obj->dismisser); - return new user($this->assignee); + $assign[] = $obj; + } + + return $assign; } /* - * Reset the seen flag and reassign this issue. + * Add an assignee to this issue. Returns false if user is + * already assigned, or if another error occurs; true + * otherwise. */ - public function assignTo(user $assignee) : void + public function addAssignee(user $assignee, user $assigner) : bool { - $this->seen = 0; - $this->assignee = $assignee->guid; - $this->assigned = self::getCurrentTimestamp(); - $this->saveObj(); + if ($assignee->isAssignedTo($this) || !isset($assignee->guid) + || !isset($assigner->guid)) + return false; + + $query = "INSERT INTO assignees (guid, assignee, assigner, assigned)" . + " VALUES ('" . database::esc($this->guid) . "', '" . + database::esc($assignee->guid) . "', '" . database::esc($assigner->guid) . + "', '" . database::esc(self::getCurrentTimestamp()) . "')"; + + database::query($query); + return true; } /* - * Get the author of this issue. This is usually the user - * that opened the issue, but may differ if this issue was - * elevated from a previous discussion thread. + * Dismiss an assignee, effectively unassigning them. Returns + * false if user is not already an active assignee, or if another + * error occurs; true otherwise. */ - public function getAuthor() : user + public function dismissAssignee(user $assignee, user $dismisser) : bool { - if (!isset($this->author) || $this->author == "") - return NULL; + if (!$assignee->isAssignedTo($this) || !isset($assignee->guid) + || !isset($dismisser->guid)) + return false; + + $query = "UPDATE assignees SET dismisser = '" . database::esc($dismisser->guid) . + "', dismissed = '" . database::esc(self::getCurrentTimestamp()) . + "' WHERE guid = '" . database::esc($this->guid) . "' AND assignee = '" . + database::esc($assignee->guid) . "'"; + + database::query($query); + return true; + } + + /* + * Signoff an assignee's portion of this issue. Returns false + * if user is not already an active assignee, or if another + * error occurs; true otherwise. + */ + public function signoffAssignee(user $assignee) : bool + { + if (!$assignee->isAssignedTo($this) || !isset($assignee->guid)) + return false; + + $query = "UPDATE assignees SET signedoff = '" . + database::esc(self::getCurrentTimestamp()) . "' WHERE guid = '" . + database::esc($this->guid) . "' AND assignee = '" . + database::esc($assignee->guid) . "'"; + + database::query($query); + return true; + } - return new user($this->author); + /* + * Get the OP message for this issue. + */ + public function getOPMesg() : mesg + { + return new mesg($this->mesg); } /* - * Set the author of this issue. This should usually only - * be done while constructing a new message or to clear out - * references to a user that got removed. + * Get all messages on this issue. The OP message is filtered + * from results. Messages are sorted by date created. */ - public function setAuthor(user $author) : void + public function getMesgs_ordByDatetime() : array { - $this->author = $author->guid; - $this->saveObj(); + $mesgs = parent::getMesgs_ordByDatetime(); + $i = -1; + + foreach ($mesgs as $k => $m) + { + if ($m->guid == $this->mesg) + { + $i = $k; + break; + } + } + + if ($i != -1) + { + unset($mesgs[$i]); + $mesgs = array_values($mesgs); + } + + return $mesgs; } /* @@ -130,15 +210,6 @@ class issue extends obj return new user($this->closer); } - /* - * Mark the user that closed this issue. - */ - public function setCloser(user $closer) : void - { - $this->closer = $closer->guid; - $this->saveObj(); - } - /* * Get the pad this issue exists under */ @@ -154,9 +225,9 @@ class issue extends obj /* * Advance this issue in the pipeline, closing it if already in the - * last stage. + * last stage. A closer is needed incase a close takes place. */ - public function advance() : void + public function advance(user $closer) : void { $stage = $this->getParent(); @@ -164,7 +235,7 @@ class issue extends obj return; if (!($next = $stage->getNext())) - $this->close(); + $this->close($closer); else $this->setParent($next); } @@ -178,8 +249,8 @@ class issue extends obj if ($pad) { + $this->closer = $closer->guid; $this->closed = self::getCurrentTimestamp(); - $this->setCloser($closer); $this->setParent($pad); } } -- cgit v1.2.3 From c2d42ce0239c8da0cb9acea922f6dea183196225 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 21 Oct 2018 23:07:10 -0400 Subject: agent: Add function isAssignedTo() --- app/class/agent.class.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'app/class') diff --git a/app/class/agent.class.php b/app/class/agent.class.php index c8e6436..4c75f0b 100644 --- a/app/class/agent.class.php +++ b/app/class/agent.class.php @@ -74,6 +74,21 @@ abstract class agent extends obj return false; } + /* + * Check whether this agent is assigned to the given issue + */ + public function isAssignedTo(issue $issue) : bool + { + foreach ($issue->getAssignees() as $assign) + { + if ($assign->assignee->guid == $this->guid + && $assign->dismissed == "") + return true; + } + + return false; + } + /* * Send an email message to this agent using stored configuration * parameters. If config is not established, delivery is not -- cgit v1.2.3 From 384f2649b714d310b385a59cc34d11fff1d85ef2 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sun, 21 Oct 2018 23:28:08 -0400 Subject: Revert "mesg: Update function makeIssue()" This reverts commit 62872702dc413b7abab94d8a5a7bd21770b5d241. --- app/class/mesg.class.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'app/class') diff --git a/app/class/mesg.class.php b/app/class/mesg.class.php index d40028c..2e5c9d6 100644 --- a/app/class/mesg.class.php +++ b/app/class/mesg.class.php @@ -266,14 +266,13 @@ class mesg extends obj * object is created and this message object will be destroyed. If * this is not an eligible message for promotion, NULL is returned. */ - public function makeIssue(user $owner, stage $parent) : ?issue + public function makeIssue(stage $parent) : ?issue { if ($this->getParent()->objtype != "pad") return NULL; - $issue = issue::initNew($this->name, $owner, $parent); - $issue->author = $this->author; - $issue->authored = $this->created; + $issue = issue::initNew($this->name, $this->getOwner(), $parent); + $issue->created = $this->created; $issue->description = $this->mesg; $issue->saveObj(); -- cgit v1.2.3 From 1bb6077c7c74c0b65be906cbeed417f911498e87 Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Wed, 24 Oct 2018 03:26:05 -0400 Subject: obj: Fix bug in function getMesgs_ordByDatetime() The SQL query here, as written, was omitting 'log' type messages from the results. Signed-off-by: Malf Furious --- app/class/obj.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/class') diff --git a/app/class/obj.class.php b/app/class/obj.class.php index 199529b..850184c 100644 --- a/app/class/obj.class.php +++ b/app/class/obj.class.php @@ -199,8 +199,8 @@ class obj extends table */ public function getMesgs_ordByDatetime() : array { - $query = "SELECT guid FROM objects WHERE objtype = 'mesg' AND " . - "parent = '" . database::esc($this->guid) . "' ORDER BY created"; + $query = "SELECT guid FROM objects WHERE (objtype = 'mesg' OR objtype = 'log') " . + "AND parent = '" . database::esc($this->guid) . "' ORDER BY created"; $res = database::query($query); $mesgs = array(); -- cgit v1.2.3 From 2ec07236fed37e5e35b44449f39abc93e854132f Mon Sep 17 00:00:00 2001 From: Malf Furious Date: Sat, 27 Oct 2018 15:37:12 -0400 Subject: Bump version number Signed-off-by: Malf Furious --- app/class/globals.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/class') diff --git a/app/class/globals.php b/app/class/globals.php index c1a6e32..74635d7 100644 --- a/app/class/globals.php +++ b/app/class/globals.php @@ -19,7 +19,7 @@ require_once "class/obj.class.php"; * These are utility functions and constants for the Scrott application. */ -define("__VERSION__", "v0.0"); +define("__VERSION__", "v0.1"); /* * These global variables are arrays of strings logged by Scrott business -- cgit v1.2.3