summaryrefslogtreecommitdiff
path: root/classes
diff options
context:
space:
mode:
authorGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-03-12 07:01:30 +0200
committerGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-03-12 07:01:30 +0200
commit47cbb529f5723b246125ae083a193e11481b89ef (patch)
tree31ab20b2fbf12bee1cd994c3d6fb822fa52622e3 /classes
downloadclassroom-47cbb529f5723b246125ae083a193e11481b89ef.tar.gz
classroom-47cbb529f5723b246125ae083a193e11481b89ef.tar.bz2
classroom-47cbb529f5723b246125ae083a193e11481b89ef.zip
initializing classroom structure using anom framework
Diffstat (limited to 'classes')
-rw-r--r--classes/Benchmark.php150
-rw-r--r--classes/Cart.php71
-rw-r--r--classes/Database.php331
-rw-r--r--classes/Registry.php129
-rw-r--r--classes/Repository.php43
-rw-r--r--classes/Request.php94
-rw-r--r--classes/Route.php97
-rw-r--r--classes/Security.php923
-rw-r--r--classes/User.php111
-rw-r--r--classes/authenticator/PasswordTrait.php40
-rw-r--r--classes/authenticator/User.php102
-rw-r--r--classes/authenticator/UserInterface.php15
-rw-r--r--classes/authenticator/UserManager.php69
-rw-r--r--classes/authenticator/UserManagerInterface.php21
-rw-r--r--classes/authenticator/UserToken.php33
-rw-r--r--classes/authenticator/UserTokenInterface.php13
-rw-r--r--classes/authenticator/info.md114
-rw-r--r--classes/cache/Cache_interface.php34
-rw-r--r--classes/cache/FileCache.php93
-rw-r--r--classes/cache/MemcachedCache.php37
-rw-r--r--classes/cache/RedisCache.php69
-rw-r--r--classes/session/DatabaseSession.php138
-rw-r--r--classes/session/DefaultSession.php35
-rw-r--r--classes/session/FilesSession.php142
-rw-r--r--classes/session/SessionHandlerInterface.php169
25 files changed, 3073 insertions, 0 deletions
diff --git a/classes/Benchmark.php b/classes/Benchmark.php
new file mode 100644
index 0000000..a7e93ea
--- /dev/null
+++ b/classes/Benchmark.php
@@ -0,0 +1,150 @@
+<?php
+
+// define('BENCHMARKED', true); // then you know if class is active
+
+class Benchmark {
+
+ private static $timeSpots = Array();
+
+ private static $memoryUse = Array();
+
+ private static $enableReport = true;
+
+
+ /** add_timespot
+ * ---
+ * add new timespot
+ * @param $key : label of timespot
+ */
+ public static function add_spot(string $key)
+ {
+ self::$timeSpots[$key] = microtime(true);
+ self::$memoryUse[$key] = round(memory_get_usage()/(1024*1024),2);
+ }
+
+
+ /** exist
+ * checks if a benchmark spot with label $key exists
+ */
+ public static function exist(string $key) : bool
+ {
+ return isset(self::$timeSpots[$key]);
+ }
+
+
+ /** desableReport
+ * (Self-explanatory)
+ */
+ public static function disableReport()
+ {
+ self::$enableReport = false;
+ }
+
+
+ /** render_benchmark_report
+ * ---
+ * renders performance report
+ * as html comment at the end of webpage in various modes
+ * @param $mode (string)
+ * + 'comment' (default): include report as html comment
+ * + 'html' : include report as a html div
+ * + 'code' : include report as a pre/code block
+ */
+ public static function render_report(string $mode = 'comment' )
+ {
+ if (self::$enableReport) {
+
+ switch ($mode) {
+ case 'html':
+ $open = '<p>';
+ $close = '</b></p>';
+ $divider = ":<b>";
+ $prefix = '<div>';
+ $suffix = '</div>';
+ break;
+ case 'code':
+ $open = '';
+ $close = '';
+ $divider = ": ";
+ $prefix = '<pre>';
+ $suffix = '</pre>';
+ break;
+ case 'comment':
+ default:
+ $open = '<!--';
+ $close = '-->';
+ $divider = ":";
+ $prefix = '';
+ $suffix = '';
+ break;
+ }
+
+ echo $prefix;
+
+ // TIME REPORT
+ // ---------------------------------------------------------------------
+ echo "\n\n{$open} Timings {$close}";
+
+ // valid from php 7.3
+ // $start = self::$timeSpots[ array_key_first(self::$timespots) ];
+ // $end = self::$timeSpots[ array_key_last(self::$timespots) ];
+ $start = self::$timeSpots['start'];
+ $end = self::$timeSpots['end'];
+ $all_dt = number_format($end - $start , 4)*1000 ." ms";
+
+
+
+ // if more than 2 timespots
+ // echo dt between each spot
+ if (count(self::$timeSpots) > 2) {
+
+ // calculate dt between spots
+ $time_results = array();
+ $prev_key = '';
+ $prev_time = 0;
+ foreach(self::$timeSpots as $key => $t) {
+ if (!$prev_time) {
+ $prev_time = $t;
+ $prev_key = $key;
+ }
+ else {
+ $dt = number_format($t - $prev_time , 4)*1000 ." ms";
+ $time_results[] = array(
+ 'part' => "{$prev_key}[..{$key}]", ///$key,
+ 'dt' => $dt
+ );
+ $prev_key = $key; //// "{$prev_key}[..{$key}]";
+ $prev_time = $t;
+ }
+ }
+
+ // echo timings
+ foreach ($time_results as $key => $val) {
+ echo "\n\t{$open} {$val['part']} {$divider} {$val['dt']} {$close}";
+ }
+ }
+
+ // echo total time
+ echo "\n\t{$open} total {$divider} {$all_dt} {$close}";
+
+
+ // MEMORY REPORT
+ // ---------------------------------------------------------------------
+ echo "\n\n{$open} Memory Usage {$close}";
+
+ if (count(self::$memoryUse) > 2) {
+
+ foreach (self::$memoryUse as $key => $val) {
+ echo "\n\t{$open} {$key} {$divider} {$val}MB {$close}";
+ }
+
+ } else {
+ echo "\n\t{$open} memory usage {$divider} ". round(memory_get_usage()/(1024*1024),2) ."MB {$close}";
+ }
+
+ echo $suffix;
+
+ }
+
+ }
+}
diff --git a/classes/Cart.php b/classes/Cart.php
new file mode 100644
index 0000000..4e6f585
--- /dev/null
+++ b/classes/Cart.php
@@ -0,0 +1,71 @@
+<?php
+
+class Cart
+{
+
+ public function checkout()
+ {
+
+ }
+
+ public function addOrder()
+ {
+ // get cart from cookie
+ // VITAL: validate + sanitize
+ //
+ // then
+ //
+ // get address and contact details from post
+ }
+
+
+ /** history
+ * get the last $limit cart-orders
+ * @param $limit (int)
+ */
+ public function history($limit)
+ {
+ // SELECT O.* , OP.* , P.*
+ // FROM orders O
+ // lEFT JOIN order_products OP ON OP.oid = O.id
+ // LEFT JOIN products P ON P.id = OP.pid
+ // WHERE uid = :uid
+ // AND O.id IN (
+ // SELECT id FROM orders WHERE uid = :uid
+ // ORDER BY id DESC
+ // )
+ //
+ // $items = runLimitQuery(sql, [uid = $this->uid], 10)
+ //
+ // oranize in results = [
+ // {
+ // oid,
+ // prods: [
+ // {
+ // pid,
+ // prodlabel,
+ // count
+ // },
+ // ...
+ // ]
+ // },
+ // ...,
+ // ]
+ //
+ // return $results
+
+ }
+}
+
+
+
+/* ---
+orders:
+ : id
+ : uid (user id)
+
+order_products
+ : id
+ : oid (order id)
+ : pid (product id)
+ -- */ \ No newline at end of file
diff --git a/classes/Database.php b/classes/Database.php
new file mode 100644
index 0000000..35ff65f
--- /dev/null
+++ b/classes/Database.php
@@ -0,0 +1,331 @@
+<?php
+/** Database Class
+ *
+ */
+class Database {
+
+ /** PROPERTIES
+ * -------------------------------------------------------------------------
+ */
+
+ private $throw_errors;
+
+ private $connection = null;
+
+ private $result = null;
+
+
+ /** METHODS
+ * -------------------------------------------------------------------------
+ */
+
+ /** __construct
+ *
+ * @param $errors: Set to true, to catch error exceptions.
+ * @return void
+ */
+ public function __construct($errors = false)
+ {
+ $this->throw_errors = PRODUCTION ? false : true;
+
+ if (null == $this->connection) {
+ try {
+ $this->connection = new PDO(
+ "mysql:" . PDO_HOST . ";" . "dbname=" . DB_NAME,
+ DB_USER,
+ DB_PASS,
+ array(
+ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
+ PDO::MYSQL_ATTR_LOCAL_INFILE => true
+ )
+ );
+
+ // handle error reporting
+ if ($this->throw_errors) {
+ $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+ $this->setTimezone();
+
+ } catch (PDOException $exc) { handle_exception($exc); }
+ }
+ # return $this->connection;
+ }
+
+
+ /** disconnect
+ * CHECK: not sure if needed
+ */
+ public function disconnect()
+ {
+ $this->connection = null;
+ }
+
+
+ /** set Timezone
+ * (Self-explanatory)
+ */
+ public function setTimezone($timezone = DB_TIMEZONE) {
+ // $this->connection->prepare($timezone)->execute();
+ }
+
+
+ /** lastInsertID
+ * returns the ID of last inserted record
+ */
+ public function lastInsertID()
+ {
+ return $this->connection->insert_id;
+ }
+
+
+ /** query
+ * ---
+ * set and execute a query safely;
+ * save results as associative array; DO NOT RETURN RESULTS
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $pypass (boolean): flag to bypass security check
+ * @return $this (database handler)
+ */
+ public function query($sql, $args=[])
+ {
+ try {
+
+ $stmt = $this->connection->prepare($sql);
+
+ if ($args == []) {
+ $result = $stmt->execute();
+
+ } else {
+ $result = $stmt->execute($args);
+
+ }
+
+ $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $this->result = $result;
+
+ return $this;
+
+ } catch (PDOException $exc) { handle_exception($exc); }
+
+ }
+
+
+ /** getAll
+ * ---
+ * return all resulted records
+ * use it after db->query();
+ */
+ public function getAll()
+ {
+ return ($this->result === null) ? false : $this->result;
+ }
+
+
+ /** getFirst
+ * ---
+ * get first row of the resulted query;
+ * used when one row is expected
+ * ex. $db->query('SELECT * FROM users WHERE id = :id',['id'=>1])->getFirst();
+ */
+ public function getFirst()
+ {
+ if (($this->result === null) || ($this->result == [])) {
+ return false;
+
+ } else { return $this->result[0]; }
+ }
+
+
+ /** getOnly
+ * ---
+ * return the first column value of the first row
+ * used when only one value is needed
+ * ex. $db->query('SELECT Count(id) FROM table',[])->getOnly();
+ */
+ public function getOnly()
+ {
+ if (($this->result === null) || ($this->result == [])) {
+ return false;
+
+ } else { return array_values($this->result[0])[0]; }
+ }
+
+
+ /** runQuery
+ * --- (shortcut method)
+ * execute a query safely;
+ * return results as associative array
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $pypass (boolean): flag to bypass security check
+ */
+ public function runQuery($sql, $args=[])
+ {
+ return $this->query($sql, $args)->getAll();
+ }
+
+
+ /** runLimitQuery( sql, args, limit=100, offset = null )
+ * set LIMIT / OFFSET clauses in a secure way
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $limit (int): LIMIT number
+ * @param $offset (int): OFFSET number
+ */
+ public function runLimitQuery($sql, $args, $limit = 100, $offset = null)
+ {
+ $limitStr = $offsetStr = "";
+
+ // construct LIMIT clause
+ if (is_int($limit)) {
+ $limitStr = " LIMIT {$limit}";
+
+ // construct OFFSET clause (when a LIMIT pre-exists)
+ if (is_int($offset)) {
+ $offsetStr =" OFFSET {$offset}";
+ }
+ }
+
+ $sql = $sql . $limitStr . $offsetStr;
+
+ return $this->runQuery($sql, $args);
+ }
+
+
+ /** insert
+ * @param $table (string): name of table
+ * @param $values: an associative of (fieldName => value) pairs
+ *
+ * example call:
+ * ---
+ * $db->insert('products',
+ * [
+ * 'title' => 'My Dark Chocolate 200g',
+ * 'text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ...',
+ * 'isFood' => 1,
+ * 'isToxic' => 0
+ * ]
+ * );
+ *
+ * ...which prepares the SQL query:
+ * INSERT INTO products (title, text, isFood, isToxic)
+ * VALUES (:title, :text, :isFood, :isToxic)
+ *
+ * ...and injects the values: [ :title => 'My Dark Chocolate 200g' , ... ]
+ */
+ public function insert($table, array $values)
+ {
+ $fieldSets = [];
+ $valueSets = [];
+ $bindSets = [];
+
+ foreach($values as $key => $val) {
+ $fieldSets[] = $key;
+ $valueSets[] =':'. $key;
+ $bindSets[':'. $key] = $val;
+ }
+
+ $sql = "INSERT INTO {$table} (". implode(', ', $fieldSets) .")
+ VALUES (". implode(', ', $valueSets) .")";
+
+ return $this->runQuery($sql, $bindSets);
+ }
+
+
+ /** update
+ * @param $table (string): name of table
+ * @param $values: an associative of (fieldName => value) pairs
+ * @param $id: an associative of (fieldName => value) index fields
+ *
+ * example call:
+ * ---
+ * $db->update('products',
+ * [ 'title' => 'My Chocolate','isFood' => 1 ],
+ * [ 'id' => 123 ]
+ * );
+ *
+ * ...which prepares the SQL query:
+ * UPDATE products SET `title` = :title, `isFood` = :isFood WHERE id = :id
+ *
+ * ...and injects: [':title'=> 'My Chocolate' , ':isFood'=> 1 , ':id'=> 123]
+ */
+ public function update( $table, array $values, array $identity)
+ {
+ $fieldSets = []; // array of field names
+ $idSets = []; // array of data-holders
+ $bindSets = []; // array of data-bindings
+
+ foreach($values as $key => $val) {
+ $fieldSets[] = "{$key} = :{$key}";
+ $bindSets[':'. $key] = $val;
+ }
+
+ foreach($identity as $key => $val) {
+ $idSets = "{$key} = :{$key}";
+ $bindSets[':'. $key] = $val;
+ }
+
+ $sql = "UPDATE {$table} SET ". implode(', ', $fieldSets)
+ ." WHERE ". implode(" AND ", $idSets);
+
+ return $this->runQuery($sql, $bindsArray);
+ }
+
+
+ /** multiInsert( table, fields , values )
+ * Construct a multiple-insert clause
+ *
+ * @param $table (string): name of table
+ * @param $fields (array): array with field-names
+ * @param $values (array): array of value-arrays
+ *
+ * example call:
+ * ---
+ * $db->multiInsert('order_products',
+ * [ 'orderID', 'productID', 'unitPrice', 'quantity', 'note' ],
+ * [
+ * [ 124, 102030, 1.25, 5, '' ],
+ * [ 124, 102040, 10.50, 2, 'some note about product #102040' ],
+ * [ 124, 102050, 7.20, 3, '' ],
+ * [ 124, 102060, 3.25, 1, 'some other note' ]
+ * ]
+ * );
+ */
+ public function multiInsert($table, array $fieldsArray, array $valuesArray)
+ {
+ if (count($fieldsArray) != count($valuesArray[0])) {
+ throw new Exception('Fields and value arrays don\'t match.');
+ }
+
+ // setup fieldsSet
+ // ex. "(Title, Price, Status)"
+ $fieldsSet = ' (`'. implode(
+ '`, `', // make sure fieldnames are not SQL-bound terms
+ str_replace('`', '', $fieldsArray) // clean fieldnames
+ ) .'`) ';
+
+ // setup holders array and bind-values array
+ // ex. "(:Title1, :Price1, :Status1), (:Title2, :Price2, :Status2), ...",
+ $holdersArray = [];
+ $bindsArray = [];
+ $counter = 1;
+ foreach($valuesArray as $key => $rowArray) {
+ $rowHolders = [];
+
+ foreach($itemArray as $key => $val) {
+ $rowHolders = ':'. $fieldsArray[$key] . $counter;
+ $bindsArray[ ':'. $fieldsArray[$key] . $counter ] = $val;
+ }
+ $holdersArray[] = '('. implode(', ', $rowHolders ) .')';
+ $counter++;
+ }
+
+ $sql = "INSERT INTO {$table}" . $fieldsSet
+ . ' VALUES '. impload(', ', $holdersArray);
+
+ return $this->runQuery($sql, $bindsArray);
+ }
+
+}
diff --git a/classes/Registry.php b/classes/Registry.php
new file mode 100644
index 0000000..df012e9
--- /dev/null
+++ b/classes/Registry.php
@@ -0,0 +1,129 @@
+<?php
+
+/** Registry
+ * ---
+ * Regisrty is a global repository of {key: value} pairs,
+ * where key is a friendly label, and...
+ * value can be anything (numbet|string|array|object etc)
+ *
+ * It has a central role in the framework.
+ * By default it holds the Request instance and several
+ * othe core instanses like database connections etc.
+ */
+class Registry
+{
+ /** STATIC PROPERTIES
+ * -------------------------------------------------------------------------
+ */
+
+ /** keeps all ready-to-use records
+ * ---
+ * These are all records defined via Registry::set(), and
+ * vow-records defined (Registry::vow()) AND called (Registry::use())
+ * (it does not include the vows that have not carried-out yet )
+ */
+ private static $records = Array();
+
+ /** keeps all records that will be carried-out later (if ever)
+ * ---
+ * These are registered via Registry::vow()
+ */
+ private static $wish = Array();
+
+
+ /** METHODS
+ * -------------------------------------------------------------------------
+ */
+
+ /** set
+ * ---
+ * store $data to the registry undel the label $key
+ * @param $key (string)
+ * @param $data *
+ */
+ static function set($key, $data)
+ {
+ // TODO: not use if needed...
+ // you may include an explicit method for reseting the key
+ // or even reVOWing a wish;
+ // or it could be just a 3rd paremeter like
+ // function set($key, $data, $reset = false) {...}
+ // function set($key, $function, $reset = false) {...}
+
+ if (!isset(self::$records[$key])) {
+
+ self::$records[$key] = $data;
+ return true;
+
+ } else { return fasle; }
+ }
+
+
+ /** get
+ * ---
+ * get data from registry,
+ * stored under the label $key
+ * @param $key (string): the label
+ * @return data (mixed)
+ */
+ static function get($key)
+ {
+ // if defined and not beeing a vow/wish, serve
+ if (isset(self::$records[$key]) && (!isset(self::$wish[$key]))) {
+
+ return self::$records[$key];
+
+ } else { return false; }
+ }
+
+
+ /** vow
+ * ---
+ * implements a promise of a function operation
+ * when/if the registry label is called;
+ * it can store a function, an object handler etc.
+ *
+ * example:
+ * Registry::vow('key', function(){ return new Obj(); })
+ *
+ * The main advantage of using a 'vow' vs a 'set' is
+ * that a 'vow' will not use system resources to prepare
+ * the data/object/handler/etc until/if-ever is called.
+ *
+ * @param $label (string): label / refference key
+ * @param function :
+ */
+ static function vow($label, $function)
+ {
+ self::$wish[$label] = $function;
+ }
+
+
+ /** use (some vow)
+ * ---
+ * this is the method that carries out
+ * the promissed function (with arguments)
+ */
+ static function use($label, $args=[])
+ {
+ // if already prepered (and is a wish) serve it
+ if (isset(self::$records[$label]) && isset(self::$wish[$label])) {
+
+ return self::$records[$label];
+
+ } else {
+
+ // otherwise if is a wish/promise
+ if (array_key_exists($label, self::$wish)) {
+
+ // prepare, store and serve it
+ $carryOut = call_user_func_array(self::$wish[$label], $args);
+ self::$records[$label] = $carryOut;
+
+ return $carryOut;
+
+ } else { return false; }
+ }
+ }
+
+}
diff --git a/classes/Repository.php b/classes/Repository.php
new file mode 100644
index 0000000..c01d0c1
--- /dev/null
+++ b/classes/Repository.php
@@ -0,0 +1,43 @@
+<?php
+
+/** Repository
+ * is an abstract class that strores entities
+ * and serves them via ::pull() method when needed
+ *
+ * it also has an ::echo() method to list the
+ */
+abstract class Repository
+{
+
+ /** all repositories (need to) have
+ * one public static array named '$repository'
+ */
+ public static $repository = [];
+
+ /** pull
+ * one entity from repository
+ * @param $entity (string): the label of the entity
+ */
+ public static function pull($entity)
+ {
+ if (array_key_exists($entity, static::$repository)) {
+
+ return static::$repository[$entity];
+
+ } else die('repository does not exist');
+
+ }
+
+
+ /** echo
+ * lists the entities of the repository;
+ * by default it only lists the labes of the entities;
+ * @param $content (bool): if true serve contents along with the labels
+ * @return (array)
+ */
+ public static function echo($content =false)
+ {
+ if ($content) return static::$repository;
+ else return array_keys(static::$repository);
+ }
+} \ No newline at end of file
diff --git a/classes/Request.php b/classes/Request.php
new file mode 100644
index 0000000..20b82c7
--- /dev/null
+++ b/classes/Request.php
@@ -0,0 +1,94 @@
+<?php
+
+class Request
+{
+
+ public $URL; // full request url
+
+ public $PATH; // Path (decoded)
+
+ public $QUERY; // Query string (decoded)
+
+ public $HOST;
+
+ public $PORT;
+
+ public $METHOD; // request method
+
+ public $TIME; // request timestamp
+
+ public $IP; // Client's IP address
+
+ public $AGENT; // Client's User Agent
+
+ public $GET = [];
+
+ public $POST = [];
+
+ public $SIGNATURE; // user/client's device signature
+
+
+
+ public function __construct($errors = false)
+ {
+ $parsed = parse_url($_SERVER['REQUEST_URI']);
+ $this->URL = $_SERVER['REQUEST_URI'];
+ $this->PATH = (!empty($parsed['path'])) ? urldecode($parsed['path']) : '';
+ $this->QUERY = (!empty($parsed['query'])) ? urldecode($parsed['query']) : false;
+ $this->HOST = $_SERVER['HTTP_HOST'];
+ $this->PORT = $_SERVER['SERVER_PORT'];
+ $this->TIME = $_SERVER['REQUEST_TIME'];
+ $this->CLI_IP = $_SERVER['REMOTE_ADDR'];
+
+ $this->METHOD = strtolower($_SERVER['REQUEST_METHOD']);
+
+ $this->GET = $_GET; // $_GET should only used
+ // to request data or specify options (never to perform
+ // system-changes) thus should not need any validation;
+ // * If (for any reason) you requide $_GET sanitization
+ // enable it later on the method's code
+
+ // $this->INTERFACE = php_sapi_name();
+
+ $this->AGENT = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
+ $this->SIGNATURE = sha1(
+ $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ . $_SERVER['HTTP_ACCEPT'] ?? ''
+ . $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
+ . $_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''
+ );
+
+ // sanitize user input
+ // if (isset($_GET)) { $this->GET = $this->sanitize($_GET); }
+ $this->GET = $this->sanitize($_GET);
+ if (isset($_POST)) { $this->POST = $this->sanitize($_POST); }
+ if (isset($_COOKIE)) { $this->COOKIE = $this->sanitize($_COOKIE); }
+
+ // check anti-CSRF token if needed
+ // (again, GET requests should not need CSRF cheking)
+ if (in_array($this->METHOD, ['post', 'put', 'patch', 'delete'])) {
+ // TODO: only if CSRF protection enabled...
+ $this->checkCsrfToken();
+ }
+
+ }
+
+
+ private function sanitize($array)
+ {
+ // TODO:
+ // ...
+ return $array;
+ }
+
+
+ public function checkCsrfToken()
+ {
+ // TODO:
+ // ...
+ // if SCRF-token is not valideted, serve 403
+ return $array;
+ }
+
+}
+
diff --git a/classes/Route.php b/classes/Route.php
new file mode 100644
index 0000000..072723c
--- /dev/null
+++ b/classes/Route.php
@@ -0,0 +1,97 @@
+<?php
+
+/** Route
+ * ---
+ * Methods:
+ * - add( expression , callback_function , method )
+ * - notFound( callback_function )
+ * - methodNotAllowed( callback_function )
+ * - run()
+ *
+ * + matches static paths
+ * + matches dynamic paths through regex expressions
+ * + takes care of request-method
+ */
+class Route {
+
+ private static $routes = Array(); // array to host routes
+ private static $notFound = null; // 404 error function
+ private static $methodNotAllowed = null; // 404 error function
+
+ /** add (route)
+ * ---
+ * @param $expression : (string) static or regex matcher
+ * @param $function : callback function to be executed if expression is matched
+ * @param $method : get / post / any
+ */
+ public static function add( $expression, $function, $method = 'get' )
+ {
+ array_push(self::$routes, Array(
+ 'expression' => $expression,
+ 'function' => $function,
+ 'method' => strtolower($method)
+ ));
+ }
+
+
+ /** notFound($function)
+ * ---
+ * @param $function : call back function to be executed
+ */
+ public static function notFound($function)
+ {
+ self::$notFound = $function;
+ }
+
+
+ /** run()
+ * ---
+ * Parse request ; Find mathing route ;
+ * then call route's function
+ * usualy a Controller::method([poarametres])
+ */
+ public static function run(Request $request)
+ {
+ // $request = Registry::get('REQUEST');
+ $path = $request->PATH; // request path
+ $method = $request->METHOD; // request method
+
+ $path_match_found = false;
+ $route_match_found = false;
+
+ foreach(self::$routes as $route) {
+
+ // If method matched check the path
+ if ($route['method'] == $method || $method == 'any') {
+
+ // Add 'find string start' automatically
+ $route['expression'] = '^'.$route['expression'];
+
+ // Add 'find string end' automatically
+ $route['expression'] = $route['expression'].'$';
+
+ // Check path match
+ if (preg_match('#'. $route['expression'] .'#', $path, $matches)) {
+
+ $route_match_found = true;
+
+ array_shift($matches); // Always remove first element. This contains the whole string
+
+ call_user_func_array($route['function'], $matches);
+ break; // Do not check other routes
+
+ }
+ }
+ }
+
+ // No matching route was found
+ if (!$route_match_found) {
+ header("HTTP/1.0 404 Not Found");
+ if (self::$notFound) {
+ call_user_func_array(self::$notFound, []);
+ }
+ }
+
+ }
+
+}
diff --git a/classes/Security.php b/classes/Security.php
new file mode 100644
index 0000000..61979db
--- /dev/null
+++ b/classes/Security.php
@@ -0,0 +1,923 @@
+<?php
+/** Security Class
+ * ---
+ * based on CodeIgniter\Security
+ * @author EllisLab Dev Team
+ * @link https://codeigniter.com/userguide3/libraries/security.html
+ * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
+ * @copyright Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license https://opensource.org/licenses/MIT MIT License
+ * @link https://codeigniter.com
+ */
+class Security
+{
+
+ // List of sanitize filename strings @var (array)
+ public $filename_bad_chars = array(
+ '../', '<!--', '-->', '<', '>',
+ "'", '"', '&', '$', '#',
+ '{', '}', '[', ']', '=',
+ ';', '?', '%20', '%22',
+ '%3c', // <
+ '%253c', // <
+ '%3e', // >
+ '%0e', // >
+ '%28', // (
+ '%29', // )
+ '%2528', // (
+ '%26', // &
+ '%24', // $
+ '%3f', // ?
+ '%3b', // ;
+ '%3d' // =
+ );
+
+ public $charset = 'UTF-8'; // Character set @var (string) Will be overridden by the constructor.
+
+ protected $_xss_hash; // XSS Hash: @var (string) Random Hash for protecting URLs.
+
+ protected $_csrf_hash; // CSRF Hash: @var (string) Random hash for Cross Site Request Forgery protection cookie
+
+ // CSRF Expire time @var (int)
+ // Expiration time for Cross Site Request Forgery protection cookie.
+ protected $_csrf_expire = 7200; // defaults to 2hours (=7200 seconds)
+
+ // CSRF Token name: @var (string) Token name for Cross Site Request Forgery protection cookie.
+ protected $_csrf_token_name = 'pi_csrf_token';
+
+ // CSRF Cookie name: @var (string) Cookie name for Cross Site Request Forgery protection cookie.
+ protected $_csrf_cookie_name = 'pi_csrf_token';
+
+ // List of never allowed strings @var (array)
+ protected $_never_allowed_str = array(
+ 'document.cookie' => '[removed]',
+ '(document).cookie' => '[removed]',
+ 'document.write' => '[removed]',
+ '(document).write' => '[removed]',
+ '.parentNode' => '[removed]',
+ '.innerHTML' => '[removed]',
+ '-moz-binding' => '[removed]',
+ '<!--' => '&lt;!--',
+ '-->' => '--&gt;',
+ '<![CDATA[' => '&lt;![CDATA[',
+ '<comment>' => '&lt;comment&gt;',
+ '<%' => '&lt;&#37;'
+ );
+
+ // List of never allowed regex replacements @var (array)
+ protected $_never_allowed_regex = array(
+ 'javascript\s*:',
+ '(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)',
+ 'expression\s*(\(|&\#40;)', // CSS and IE
+ 'vbscript\s*:', // IE, surprise!
+ 'wscript\s*:', // IE
+ 'jscript\s*:', // IE
+ 'vbs\s*:', // IE
+ 'Redirect\s+30\d',
+ "([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?"
+ );
+
+
+ /** Class constructor
+ * ---
+ * @return void
+ */
+ public function __construct($charset = 'UTF-8')
+ {
+ $this->charset = $charset;
+
+ /* ---
+ // Is CSRF protection enabled?
+ if (CSRF_PROTCTION') && ! is_cli())
+ { --*/
+ /**************
+ // CSRF config
+ foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key)
+ {
+ //// if (NULL !== ($val = config_item($key)))
+ //// {
+ $this->{'_'.$key} = $val;
+ //// }
+ }
+
+ // Append application specific cookie prefix
+ //// if ($cookie_prefix = config_item('cookie_prefix'))
+ //// {
+ $this->_csrf_cookie_name = $cookie_prefix . $this->_csrf_cookie_name;
+ //// }
+
+ // Set the CSRF hash
+ $this->_csrf_set_hash();
+ $this->csrf_verify();
+ ***************/
+ /* ---
+ } --*/
+
+ //// log_message('info', 'Security Class Initialized');
+ }
+
+
+ /** CSRF Verify
+ * ---
+ * @return Security
+ */
+ public function csrf_verify()
+ {
+ // If it's not a POST request we will set the CSRF cookie
+ if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
+ {
+ return $this->csrf_set_cookie();
+ }
+
+ /// // Check if URI has been whitelisted from CSRF checks
+ /// if ($exclude_uris = config_item('csrf_exclude_uris'))
+ /// {
+ /// $uri = load_class('URI', 'core');
+ /// foreach ($exclude_uris as $excluded)
+ /// {
+ /// if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string()))
+ /// {
+ /// return $this;
+ /// }
+ /// }
+ /// }
+
+ // Check CSRF token validity, but don't error on mismatch just yet - we'll want to regenerate
+ $valid = isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
+ && is_string($_POST[$this->_csrf_token_name]) && is_string($_COOKIE[$this->_csrf_cookie_name])
+ && hash_equals($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]);
+
+ // We kill this since we're done and we don't want to pollute the _POST array
+ unset($_POST[$this->_csrf_token_name]);
+
+ // Regenerate on every submission?
+ if (config_item('csrf_regenerate'))
+ {
+ // Nothing should last forever
+ unset($_COOKIE[$this->_csrf_cookie_name]);
+ $this->_csrf_hash = NULL;
+ }
+
+ $this->_csrf_set_hash();
+ $this->csrf_set_cookie();
+
+ if ($valid !== TRUE)
+ {
+ $this->csrf_show_error();
+ }
+
+ log_message('info', 'CSRF token verified');
+ return $this;
+ }
+
+
+ /** CSRF Set Cookie
+ * ---
+ * @codeCoverageIgnore
+ * @return Security
+ */
+ public function csrf_set_cookie()
+ {
+ $expire = time() + $this->_csrf_expire;
+ $secure_cookie = (bool) config_item('cookie_secure');
+
+ if ($secure_cookie && ! is_https())
+ {
+ return FALSE;
+ }
+
+ // php7.3+
+ setcookie(
+ $this->_csrf_cookie_name,
+ $this->_csrf_hash,
+ array(
+ 'expires' => $expire,
+ 'path' => config_item('cookie_path'),
+ 'domain' => config_item('cookie_domain'),
+ 'secure' => $secure_cookie,
+ 'httponly' => config_item('cookie_httponly'),
+ 'samesite' => 'Strict'
+ )
+ );
+
+ log_message('info', 'CSRF cookie sent');
+
+ return $this;
+ }
+
+
+ /** Show CSRF Error
+ * --
+ * @return void
+ */
+ public function csrf_show_error()
+ {
+ show_error('The action you have requested is not allowed.', 403);
+ }
+
+
+ /** Get CSRF Hash
+ * ---
+ * @see CI_Security::$_csrf_hash
+ * @return string CSRF hash
+ */
+ public function get_csrf_hash()
+ {
+ return $this->_csrf_hash;
+ }
+
+
+ /** Get CSRF Token Name
+ * ---
+ * @see CI_Security::$_csrf_token_name
+ * @return string CSRF token name
+ */
+ public function get_csrf_token_name()
+ {
+ return $this->_csrf_token_name;
+ }
+
+
+ /** XSS Clean
+ * ---
+ * Sanitizes data so that Cross Site Scripting Hacks can be
+ * prevented. This method does a fair amount of work but
+ * it is extremely thorough, designed to prevent even the
+ * most obscure XSS attempts. Nothing is ever 100% foolproof,
+ * of course, but I haven't been able to get anything passed
+ * the filter.
+ *
+ * Note: Should only be used to deal with data upon submission.
+ * It's not something that should be used for general
+ * runtime processing.
+ *
+ * @link http://channel.bitflux.ch/wiki/XSS_Prevention
+ * Based in part on some code and ideas from Bitflux.
+ *
+ * @link http://ha.ckers.org/xss.html
+ * To help develop this script I used this great list of
+ * vulnerabilities along with a few other hacks I've
+ * harvested from examining vulnerabilities in other programs.
+ *
+ * @param string|string[] $str Input data
+ * @param bool $is_image Whether the input is an image
+ * @return string
+ */
+ public function xss_clean($str, $is_image = FALSE)
+ {
+ // Is the string an array?
+ if (is_array($str))
+ {
+ foreach ($str as $key => &$value)
+ {
+ $str[$key] = $this->xss_clean($value);
+ }
+
+ return $str;
+ }
+
+ // Remove Invisible Characters
+ $str = remove_invisible_characters($str);
+
+ // URL Decode
+ // Just in case stuff like this is submitted:
+ // <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
+ // NOTE: Use rawurldecode() so it does not remove plus signs
+ if (stripos($str, '%') !== false)
+ {
+ do
+ {
+ $oldstr = $str;
+ $str = rawurldecode($str);
+ $str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str);
+ }
+ while ($oldstr !== $str);
+ unset($oldstr);
+ }
+
+ // Convert character entities to ASCII
+ // ---
+ // This permits our tests below to work reliably.
+ // We only convert entities that are within tags since
+ // these are the ones that will pose security problems.
+ $str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
+ $str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);
+
+ // Remove Invisible Characters Again!
+ $str = remove_invisible_characters($str);
+
+
+ // Convert all tabs to spaces
+ // ---
+ // This prevents strings like this: ja vascript
+ // NOTE: we deal with spaces between characters later.
+ // NOTE: preg_replace was found to be amazingly slow here on
+ // large blocks of data, so we use str_replace.
+ $str = str_replace("\t", ' ', $str);
+
+ // Capture converted string for later comparison
+ $converted_string = $str;
+
+ // Remove Strings that are never allowed
+ $str = $this->_do_never_allowed($str);
+
+
+ // Makes PHP tags safe
+ // NOTE: XML tags are inadvertently replaced too:
+ // <?xml
+ // But it doesn't seem to pose a problem.
+ if ($is_image === TRUE)
+ {
+ // Images have a tendency to have the PHP short opening and
+ // closing tags every so often so we skip those and only
+ // do the long opening tags.
+ $str = preg_replace('/<\?(php)/i', '&lt;?\\1', $str);
+ }
+ else
+ {
+ $str = str_replace(array('<?', '?'.'>'), array('&lt;?', '?&gt;'), $str);
+ }
+
+ // Compact any exploded words
+ // ---
+ // This corrects words like: j a v a s c r i p t
+ // These words are compacted back to their correct state.
+ $words = array(
+ 'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
+ 'vbs', 'script', 'base64', 'applet', 'alert', 'document',
+ 'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
+ );
+
+ foreach ($words as $word)
+ {
+ $word = implode('\s*', str_split($word)).'\s*';
+
+ // We only want to do this when it is followed by a non-word character
+ // That way valid stuff like "dealer to" does not become "dealerto"
+ $str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
+ }
+
+
+ // Remove disallowed Javascript in links or img tags
+ // We used to do some version comparisons and use of stripos(),
+ // but it is dog slow compared to these simplified non-capturing
+ // preg_match(), especially if the pattern exists in the string
+ //
+ // Note: It was reported that not only space characters, but all in
+ // the following pattern can be parsed as separators between a tag name
+ // and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
+ // ... however, remove_invisible_characters() above already strips the
+ // hex-encoded ones, so we'll skip them below.
+ do
+ {
+ $original = $str;
+
+ if (preg_match('/<a/i', $str))
+ {
+ $str = preg_replace_callback('#<a(?:rea)?[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
+ }
+
+ if (preg_match('/<img/i', $str))
+ {
+ $str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
+ }
+
+ if (preg_match('/script|xss/i', $str))
+ {
+ $str = preg_replace('#</*(?:script|xss).*?>#si', '[removed]', $str);
+ }
+ }
+ while ($original !== $str);
+ unset($original);
+
+
+ // Sanitize naughty HTML elements
+ // ---
+ // If a tag containing any of the words in the list
+ // below is found, the tag gets converted to entities.
+ // So this: <blink>
+ // Becomes: &lt;blink&gt;
+ $pattern = '#'
+ .'<((?<slash>/*\s*)((?<tagName>[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character
+ .'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator
+ // optional attributes
+ .'(?<attributes>(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons
+ .'[^\s\042\047>/=]+' // attribute characters
+ // optional attribute-value
+ .'(?:\s*=' // attribute-value separator
+ .'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value
+ .')?' // end optional attribute-value group
+ .')*)' // end optional attributes group
+ .'[^>]*)(?<closeTag>\>)?#isS';
+
+ // Note: It would be nice to optimize this for speed, BUT
+ // only matching the naughty elements here results in
+ // false positives and in turn - vulnerabilities!
+ do
+ {
+ $old_str = $str;
+ $str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str);
+ }
+ while ($old_str !== $str);
+ unset($old_str);
+
+ // Sanitize naughty scripting elements
+ // ---
+ // Similar to above, only instead of looking for
+ // tags it looks for PHP and JavaScript commands
+ // that are disallowed. Rather than removing the
+ // code, it simply converts the parenthesis to entities
+ // rendering the code un-executable.
+ // * For example: eval('some code')
+ // ... Becomes: eval&#40;'some code'&#41;
+ $str = preg_replace(
+ '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
+ '\\1\\2&#40;\\3&#41;',
+ $str
+ );
+
+ // Same thing, but for "tag functions" (e.g. eval`some code`)
+ // See https://github.com/bcit-ci/CodeIgniter/issues/5420
+ $str = preg_replace(
+ '#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si',
+ '\\1\\2&#96;\\3&#96;',
+ $str
+ );
+
+ // Final clean up
+ // This adds a bit of extra precaution in case
+ // something got through the above filters
+ $str = $this->_do_never_allowed($str);
+
+ // Images are Handled in a Special Way
+ // - Essentially, we want to know that after all of the character
+ // conversion is done whether any unwanted, likely XSS, code was found.
+ // If not, we return TRUE, as the image is clean.
+ // However, if the string post-conversion does not matched the
+ // string post-removal of XSS, then it fails, as there was unwanted XSS
+ // code found and removed/changed during processing.
+ if ($is_image === TRUE)
+ {
+ return ($str === $converted_string);
+ }
+
+ return $str;
+ }
+
+
+ /** XSS Hash
+ * ---
+ * Generates the XSS hash if needed and returns it.
+ * @see CI_Security::$_xss_hash
+ * @return string XSS hash
+ */
+ public function xss_hash()
+ {
+ if ($this->_xss_hash === NULL)
+ {
+ $rand = $this->get_random_bytes(16);
+ $this->_xss_hash = ($rand === FALSE)
+ ? md5(uniqid(mt_rand(), TRUE))
+ : bin2hex($rand);
+ }
+
+ return $this->_xss_hash;
+ }
+
+
+ /** Get random bytes
+ * ---
+ * @param int $length Output length
+ * @return string
+ */
+ public function get_random_bytes($length)
+ {
+ if (empty($length) OR ! ctype_digit((string) $length))
+ {
+ return FALSE;
+ }
+
+ if (function_exists('random_bytes'))
+ {
+ try
+ {
+ // The cast is required to avoid TypeError
+ return random_bytes((int) $length);
+ }
+ catch (Exception $e)
+ {
+ // If random_bytes() can't do the job, we can't either ...
+ // There's no point in using fallbacks.
+ log_message('error', $e->getMessage());
+ return FALSE;
+ }
+ }
+
+ // Unfortunately, none of the following PRNGs is guaranteed to exist ...
+ if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== FALSE)
+ {
+ return $output;
+ }
+
+ if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== FALSE)
+ {
+ // Try not to waste entropy ...
+ stream_set_chunk_size($fp, $length);
+ $output = fread($fp, $length);
+ fclose($fp);
+ if ($output !== FALSE)
+ {
+ return $output;
+ }
+ }
+
+ if (function_exists('openssl_random_pseudo_bytes'))
+ {
+ return openssl_random_pseudo_bytes($length);
+ }
+
+ return FALSE;
+ }
+
+
+ /** HTML Entities Decode
+ * ---
+ * A replacement for html_entity_decode()
+ *
+ * The reason we are not using html_entity_decode() by itself is because
+ * while it is not technically correct to leave out the semicolon
+ * at the end of an entity most browsers will still interpret the entity
+ * correctly. html_entity_decode() does not convert entities without
+ * semicolons, so we are left with our own little solution here. Bummer.
+ *
+ * @link https://secure.php.net/html-entity-decode
+ *
+ * @param string $str Input
+ * @param string $charset Character set
+ * @return string
+ */
+ public function entity_decode($str, $charset = NULL)
+ {
+ if (strpos($str, '&') === FALSE)
+ {
+ return $str;
+ }
+
+ static $_entities;
+
+ isset($charset) OR $charset = $this->charset;
+ isset($_entities) OR $_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, ENT_COMPAT | ENT_HTML5, $charset));
+
+ do
+ {
+ $str_compare = $str;
+
+ // Decode standard entities, avoiding false positives
+ if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches))
+ {
+ $replace = array();
+ $matches = array_unique(array_map('strtolower', $matches[0]));
+ foreach ($matches as &$match)
+ {
+ if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE)
+ {
+ $replace[$match] = $char;
+ }
+ }
+
+ $str = str_replace(array_keys($replace), array_values($replace), $str);
+ }
+
+ // Decode numeric & UTF16 two byte entities
+ $str = html_entity_decode(
+ preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str),
+ ENT_COMPAT | ENT_HTML5,
+ $charset
+ );
+ }
+ while ($str_compare !== $str);
+ return $str;
+ }
+
+
+ /** Sanitize Filename
+ * ---
+ * @param string $str Input file name
+ * @param bool $relative_path Whether to preserve paths
+ * @return string
+ */
+ public function sanitize_filename($str, $relative_path = FALSE)
+ {
+ $bad = $this->filename_bad_chars;
+
+ if ( ! $relative_path)
+ {
+ $bad[] = './';
+ $bad[] = '/';
+ }
+
+ $str = remove_invisible_characters($str, FALSE);
+
+ do
+ {
+ $old = $str;
+ $str = str_replace($bad, '', $str);
+ }
+ while ($old !== $str);
+
+ return stripslashes($str);
+ }
+
+
+ /** Strip Image Tags
+ * ---
+ * @param string $str
+ * @return string
+ */
+ public function strip_image_tags($str)
+ {
+ return preg_replace(
+ array(
+ '#<img[\s/]+.*?src\s*=\s*(["\'])([^\\1]+?)\\1.*?\>#i',
+ '#<img[\s/]+.*?src\s*=\s*?(([^\s"\'=<>`]+)).*?\>#i'
+ ),
+ '\\2',
+ $str
+ );
+ }
+
+
+ /** URL-decode taking spaces into account
+ * ---
+ * @see https://github.com/bcit-ci/CodeIgniter/issues/4877
+ * @param array $matches
+ * @return string
+ */
+ protected function _urldecodespaces($matches)
+ {
+ $input = $matches[0];
+ $nospaces = preg_replace('#\s+#', '', $input);
+ return ($nospaces === $input)
+ ? $input
+ : rawurldecode($nospaces);
+ }
+
+
+ /** Compact Exploded Words
+ * ---
+ * Callback method for xss_clean() to remove whitespace from
+ * things like 'j a v a s c r i p t'.
+ *
+ * @used-by Security::xss_clean()
+ * @param array $matches
+ * @return string
+ */
+ protected function _compact_exploded_words($matches)
+ {
+ return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
+ }
+
+
+ /** Sanitize Naughty HTML
+ * ---
+ * Callback method for xss_clean() to remove naughty HTML elements.
+ * @used-by Security::xss_clean()
+ * @param array $matches
+ * @return string
+ */
+ protected function _sanitize_naughty_html($matches)
+ {
+ static $naughty_tags = array(
+ 'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound',
+ 'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer',
+ 'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object',
+ 'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss'
+ );
+
+ static $evil_attributes = array(
+ 'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime'
+ );
+
+ // First, escape unclosed tags
+ if (empty($matches['closeTag']))
+ {
+ return '&lt;'.$matches[1];
+ }
+ // Is the element that we caught naughty? If so, escape it
+ elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE))
+ {
+ return '&lt;'.$matches[1].'&gt;';
+ }
+ // For other tags, see if their attributes are "evil" and strip those
+ elseif (isset($matches['attributes']))
+ {
+ // We'll store the already filtered attributes here
+ $attributes = array();
+
+ // Attribute-catching pattern
+ $attributes_pattern = '#'
+ .'(?<name>[^\s\042\047>/=]+)' // attribute characters
+ // optional attribute-value
+ .'(?:\s*=(?<value>[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator
+ .'#i';
+
+ // Blacklist pattern for evil attribute names
+ $is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i';
+
+ // Each iteration filters a single attribute
+ do
+ {
+ // Strip any non-alpha characters that may precede an attribute.
+ // Browsers often parse these incorrectly and that has been a
+ // of numerous XSS issues we've had.
+ $matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']);
+
+ if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE))
+ {
+ // No (valid) attribute found? Discard everything else inside the tag
+ break;
+ }
+
+ if (
+ // Is it indeed an "evil" attribute?
+ preg_match($is_evil_pattern, $attribute['name'][0])
+ // Or does it have an equals sign, but no value and not quoted? Strip that too!
+ OR (trim($attribute['value'][0]) === '')
+ )
+ {
+ $attributes[] = 'xss=removed';
+ }
+ else
+ {
+ $attributes[] = $attribute[0][0];
+ }
+
+ $matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0]));
+ }
+ while ($matches['attributes'] !== '');
+
+ $attributes = empty($attributes)
+ ? ''
+ : ' '.implode(' ', $attributes);
+ return '<'.$matches['slash'].$matches['tagName'].$attributes.'>';
+ }
+
+ return $matches[0];
+ }
+
+
+ /** JS Link Removal
+ * ---
+ * Callback method for xss_clean() to sanitize links.
+ *
+ * This limits the PCRE backtracks, making it more performance friendly
+ * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
+ * PHP 5.2+ on link-heavy strings.
+ *
+ * @used-by CI_Security::xss_clean()
+ * @param array $match
+ * @return string
+ */
+ protected function _js_link_removal($match)
+ {
+ return str_replace(
+ $match[1],
+ preg_replace(
+ '#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|d\s*a\s*t\s*a\s*:)#si',
+ '',
+ $this->_filter_attributes($match[1])
+ ),
+ $match[0]
+ );
+ }
+
+
+ /** JS Image Removal
+ * ---
+ * Callback method for xss_clean() to sanitize image tags.
+ *
+ * This limits the PCRE backtracks, making it more performance friendly
+ * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
+ * PHP 5.2+ on image tag heavy strings.
+ *
+ * @used-by Security::xss_clean()
+ * @param array $match
+ * @return string
+ */
+ protected function _js_img_removal($match)
+ {
+ return str_replace(
+ $match[1],
+ preg_replace(
+ '#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|base64\s*,)#si',
+ '',
+ $this->_filter_attributes($match[1])
+ ),
+ $match[0]
+ );
+ }
+
+
+ /** Attribute Conversion
+ * ---
+ * @used-by CI_Security::xss_clean()
+ * @param array $match
+ * @return string
+ */
+ protected function _convert_attribute($match)
+ {
+ return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
+ }
+
+
+ /** Filter Attributes
+ * ---
+ * Filters tag attributes for consistency and safety.
+ * @used-by CI_Security::_js_img_removal()
+ * @used-by CI_Security::_js_link_removal()
+ * @param string $str
+ * @return string
+ */
+ protected function _filter_attributes($str)
+ {
+ $out = '';
+ if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
+ {
+ foreach ($matches[0] as $match)
+ {
+ $out .= preg_replace('#/\*.*?\*/#s', '', $match);
+ }
+ }
+
+ return $out;
+ }
+
+
+ /** HTML Entity Decode Callback
+ * ---
+ * @used-by CI_Security::xss_clean()
+ * @param array $match
+ * @return string
+ */
+ protected function _decode_entity($match)
+ {
+ // Protect GET variables in URLs
+ // 901119URL5918AMP18930PROTECT8198
+ $match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]);
+
+ // Decode, then un-protect URL GET vars
+ return str_replace(
+ $this->xss_hash(),
+ '&',
+ $this->entity_decode($match, $this->charset)
+ );
+ }
+
+
+ /** Do Never Allowed
+ * ---
+ * @used-by CI_Security::xss_clean()
+ * @param string
+ * @return string
+ */
+ protected function _do_never_allowed($str)
+ {
+ $str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str);
+
+ foreach ($this->_never_allowed_regex as $regex)
+ {
+ $str = preg_replace('#'.$regex.'#is', '[removed]', $str);
+ }
+
+ return $str;
+ }
+
+
+ /** Set CSRF Hash and Cookie
+ * ---
+ * @return string
+ */
+ protected function _csrf_set_hash()
+ {
+ if ($this->_csrf_hash === NULL)
+ {
+ // If the cookie exists we will use its value.
+ // We don't necessarily want to regenerate it with
+ // each page load since a page could contain embedded
+ // sub-pages causing this feature to fail
+ if (isset($_COOKIE[$this->_csrf_cookie_name]) && is_string($_COOKIE[$this->_csrf_cookie_name])
+ && preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1)
+ {
+ return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name];
+ }
+
+ $rand = $this->get_random_bytes(16);
+ $this->_csrf_hash = ($rand === FALSE)
+ ? md5(uniqid(mt_rand(), TRUE))
+ : bin2hex($rand);
+ }
+
+ return $this->_csrf_hash;
+ }
+}
diff --git a/classes/User.php b/classes/User.php
new file mode 100644
index 0000000..65b5018
--- /dev/null
+++ b/classes/User.php
@@ -0,0 +1,111 @@
+<?php
+
+class User_depricated
+{
+
+ /** PROPERTIES
+ * -------------------------------------------------------------------------
+ */
+
+ private $isLogged = false;
+
+ private $who = Array(
+ 'title' => '',
+ 'name' => '',
+ 'middle'=> '',
+ 'surname' =>'',
+ 'email' => ''
+ );
+
+ private $addresses = Array();
+
+
+
+ /** METHODS
+ * -------------------------------------------------------------------------
+ */
+
+
+ public function create()
+ {
+
+ }
+
+
+ public function setDefaultAddress($id)
+ {
+
+ }
+
+
+ public function addAddress()
+ {
+ // update session data
+ // update user record
+ }
+
+
+ public function changePassword()
+ {
+
+ }
+
+
+ public function confirmEmail()
+ {
+
+ }
+
+
+ public function sendEmail()
+ {
+
+ }
+
+ /** sendOTP()
+ * sent One-Time-Password
+ *
+ */
+ public function sendOTP($ttl)
+ {
+ $to = $this->who;
+ $otp = rand(10000,99999);
+ $expire = time() + $ttl;
+
+ $mail = new PHPMailer(true);
+ try {
+ //Server settings
+ // ...
+
+ //Recipients
+ $mail->setFrom('from@example.com', 'Mailer');
+ $mail->addAddress($to['email'], $to['surname'] .' '. $to['name']); //Add a recipient
+ $mail->addReplyTo('info@example.com', 'Information');
+
+ //Attachments
+ $mail->addAttachment('/var/tmp/file.tar.gz'); //Add attachments
+ $mail->addAttachment('/tmp/image.jpg', 'new.jpg'); //Optional name
+
+ //Content
+ $mail->isHTML(true); //Set email format to HTML
+ $mail->Subject = 'Your OTP';
+ $mail->Body = 'This is the HTML message body <b>in bold!</b>';
+ $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
+
+ $mail->send();
+ echo 'Message has been sent';
+ } catch (Exception $e) {
+ echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
+ }
+
+ }
+
+ /** updateSession()
+ * regenerates session
+ */
+ public function updateSession()
+ {
+
+ }
+
+}
diff --git a/classes/authenticator/PasswordTrait.php b/classes/authenticator/PasswordTrait.php
new file mode 100644
index 0000000..22976d8
--- /dev/null
+++ b/classes/authenticator/PasswordTrait.php
@@ -0,0 +1,40 @@
+<?php
+
+
+// namespace DevCoder\Authentication\Core;
+//
+// use DevCoder\Authentication\UserInterface;
+
+/**
+ * Trait PasswordTrait
+ * @package DevCoder\Authentication\Core
+ */
+trait PasswordTrait
+{
+ private $cost = 10;
+
+ public function cryptPassword(string $plainPassword): string
+ {
+ return password_hash(
+ $plainPassword,
+ PASSWORD_BCRYPT,
+ ['cost' => $this->cost]
+ );
+ }
+
+ public function isPasswordValid(UserInterface $user, string $plainPassword): bool
+ {
+ return password_verify(
+ $plainPassword,
+ $user->getPassword()
+ );
+ }
+
+ public function setCost(int $cost): void
+ {
+ if ($cost < 4 || $cost > 12) {
+ throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
+ }
+ $this->cost = $cost;
+ }
+}
diff --git a/classes/authenticator/User.php b/classes/authenticator/User.php
new file mode 100644
index 0000000..b99fa70
--- /dev/null
+++ b/classes/authenticator/User.php
@@ -0,0 +1,102 @@
+<?php
+
+
+// namespace DevCoder\Authentication;
+
+class User implements UserInterface
+{
+
+ // @var string
+ private $userName;
+
+ // @var string
+ private $password;
+
+ // @var array
+ private $roles = [];
+
+ // @var bool
+ private $enabled = true;
+
+
+ /** GETs
+ * -------------------------------------------------------------------------
+ */
+
+ /** getUserName
+ * @return null|string
+ */
+ public function getUsername(): ?string
+ {
+ return $this->userName;
+ }
+
+ /** getPassword
+ * @return null|string
+ */
+ public function getPassword(): ?string
+ {
+ return $this->password;
+ }
+
+ /** getRoles
+ * @return array
+ */
+ public function getRoles(): array
+ {
+ return $this->roles;
+ }
+
+ /** isEnabled
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->enabled;
+ }
+
+
+ /** SETs
+ * -------------------------------------------------------------------------
+ */
+
+ /** setUserName
+ * @param string $userName
+ * @return User
+ */
+ public function setUserName(string $userName): self
+ {
+ $this->userName = $userName;
+ return $this;
+ }
+
+ /** setPassword
+ * @param string $password
+ * @return User
+ */
+ public function setPassword(string $password): self
+ {
+ $this->password = $password;
+ return $this;
+ }
+
+ /** setRoles
+ * @param array $roles
+ * @return User
+ */
+ public function setRoles(array $roles): self
+ {
+ $this->roles = $roles;
+ return $this;
+ }
+
+ /**
+ * @param bool $enabled
+ * @return User
+ */
+ public function setEnabled(bool $enabled): self
+ {
+ $this->enabled = $enabled;
+ return $this;
+ }
+}
diff --git a/classes/authenticator/UserInterface.php b/classes/authenticator/UserInterface.php
new file mode 100644
index 0000000..5cc6d2d
--- /dev/null
+++ b/classes/authenticator/UserInterface.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Interface UserInterface
+ * @package DevCoder\Authentication
+ */
+interface UserInterface
+{
+ public function getUsername() :?string;
+
+ public function getPassword() :?string;
+
+ public function getRoles() : array;
+
+ public function isEnabled(): bool;
+} \ No newline at end of file
diff --git a/classes/authenticator/UserManager.php b/classes/authenticator/UserManager.php
new file mode 100644
index 0000000..36f0d16
--- /dev/null
+++ b/classes/authenticator/UserManager.php
@@ -0,0 +1,69 @@
+<?php
+
+// namespace DevCoder\Authentication\Core;
+//
+// use DevCoder\Authentication\Token\UserToken;
+// use DevCoder\Authentication\Token\UserTokenInterface;
+// use DevCoder\Authentication\UserInterface;
+
+/**
+ * Class UserManager
+ * @package DevCoder\Authentication\Core
+ */
+class UserManager implements UserManagerInterface
+{
+
+ use PasswordTrait;
+
+ public function __construct()
+ {
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ }
+ }
+
+ public function getUserToken(): ?UserTokenInterface
+ {
+ $userToken = null;
+ if ($this->hasUserToken()) {
+ $userToken = unserialize($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
+ }
+
+ return $userToken;
+ }
+
+ public function hasUserToken(): bool
+ {
+ $key = UserTokenInterface::DEFAULT_PREFIX_KEY;
+ return (array_key_exists($key, $_SESSION) && unserialize($_SESSION[$key]) !== false);
+ }
+
+ public function isGranted(array $roles): bool
+ {
+ if (!is_null($userToken = $this->getUserToken())) {
+ return false;
+ }
+
+ if ($userToken->getUser() instanceof UserInterface) {
+ return (!empty(array_intersect($roles, $userToken->getUser()->getRoles())));
+ }
+
+ return false;
+ }
+
+ public function createUserToken(UserInterface $user): UserTokenInterface
+ {
+ $userToken = new UserToken($user);
+ $_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY] = $userToken->serialize();
+
+ return $userToken;
+ }
+
+ public function logout(): void
+ {
+ if ($this->hasUserToken()) {
+ unset($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
+ }
+ }
+}
+
diff --git a/classes/authenticator/UserManagerInterface.php b/classes/authenticator/UserManagerInterface.php
new file mode 100644
index 0000000..ef120c1
--- /dev/null
+++ b/classes/authenticator/UserManagerInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+// namespace DevCoder\Authentication\Core;
+//
+// use DevCoder\Authentication\Token\UserTokenInterface;
+// use DevCoder\Authentication\UserInterface;
+
+interface UserManagerInterface
+{
+ public function getUserToken(): ?UserTokenInterface;
+
+ public function hasUserToken(): bool;
+
+ public function createUserToken(UserInterface $user): UserTokenInterface;
+
+ public function logout(): void;
+
+ public function cryptPassword(string $plainPassword): string;
+
+ public function isPasswordValid(UserInterface $user, string $plainPassword): bool;
+}
diff --git a/classes/authenticator/UserToken.php b/classes/authenticator/UserToken.php
new file mode 100644
index 0000000..7758086
--- /dev/null
+++ b/classes/authenticator/UserToken.php
@@ -0,0 +1,33 @@
+<?php
+
+
+// namespace DevCoder\Authentication\Token;
+//
+// use DevCoder\Authentication\UserInterface;
+
+/**
+ * Class UserToken
+ * @package DevCoder\Authentication\Token
+ */
+class UserToken implements UserTokenInterface
+{
+ /**
+ * @var UserInterface
+ */
+ private $user;
+
+ public function __construct(UserInterface $user)
+ {
+ $this->user = $user;
+ }
+
+ public function getUser(): UserInterface
+ {
+ return $this->user;
+ }
+
+ public function serialize(): string
+ {
+ return serialize($this);
+ }
+}
diff --git a/classes/authenticator/UserTokenInterface.php b/classes/authenticator/UserTokenInterface.php
new file mode 100644
index 0000000..2a21d8f
--- /dev/null
+++ b/classes/authenticator/UserTokenInterface.php
@@ -0,0 +1,13 @@
+<?php
+/**
+ * Interface UserTokenInterface
+ * @package DevCoder\Authentication\Token
+ */
+interface UserTokenInterface
+{
+ const DEFAULT_PREFIX_KEY = 'user_security';
+
+ public function getUser(): UserInterface;
+
+ public function serialize(): string;
+} \ No newline at end of file
diff --git a/classes/authenticator/info.md b/classes/authenticator/info.md
new file mode 100644
index 0000000..e6d1372
--- /dev/null
+++ b/classes/authenticator/info.md
@@ -0,0 +1,114 @@
+# Authentication System
+
+read/ref: https://dev.to/fadymr/php-create-your-own-php-authentication-4e20
+
+also for session: https://dev.to/fadymr/php-create-a-simple-session-wrapper-class-dpk
+
+
+
+## How to use ?
+
+
+### Registration
+
+
+ // use DevCoder\Authentication\Core\UserManager;
+ // use DevCoder\Authentication\User;
+
+ // register
+ $userManager = new UserManager();
+ $req = Registry::get('REQUEST);
+
+ $password = $userManager->cryptPassword($req->POST['password']);
+
+ $user = (new User())
+ ->setUserName($req->POST['username'])
+ ->setPassword($password)
+ ->setRoles(['ROLE_USER']);
+
+ $userManager->createUserToken($user);
+
+ // check Token in Session
+ var_dump($userManager->getUserToken());
+ // object(DevCoder\Authentication\Token\UserToken)[4]
+ // private 'user' =>
+ // object(DevCoder\Authentication\User)[5]
+ // private 'userName' => string 'username' (length=8)
+ // private 'password' => string '$2y$10$iWdcmebmikUFlgKMqW7/rOmUp1DjFAuWKqdUHBhL08FZ7LL6bwRey' (length=60)
+ // private 'roles' =>
+ // array (size=1)
+ // 0 => string 'ROLE_USER' (length=9)
+ // private 'enabled' => boolean true
+
+
+## Connected or not
+
+ <?php
+
+ // use DevCoder\Authentication\Core\UserManager;
+ // use DevCoder\Authentication\User;
+
+ $userManager = new UserManager();
+ if ($userManager->hasUserToken()) {
+ // connected
+
+ $token = $userManager->getUserToken();
+ $user = $token->getUser();
+ var_dump($user);
+ // object(DevCoder\Authentication\User)[5]
+ // private 'userName' => string 'username' (length=8)
+ // private 'password' => string '$2y$10$OBobeLhdvdiftuedlv1a6e4.qF6sCG/usq5WEV4E3uB.UiS1egv/m' (length=60)
+ // private 'roles' =>
+ // array (size=1)
+ // 0 => string 'ROLE_USER' (length=9)
+ // private 'enabled' => boolean true
+
+ } else {
+ // not connected
+ }
+
+
+## Access management
+
+ $userManager = new UserManager();
+ if ($userManager->isGranted(['ROLE_ADMIN'])) {
+ //is admin
+ // return Response 200
+ }else {
+ //is not admin
+ // return Response 403
+ }
+
+ Logout
+
+ $userManager = new UserManager();
+ $userManager->logout();
+
+
+## Login
+
+ <?php
+
+ use DevCoder\Authentication\Core\UserManager;
+
+ $stmt = $pdo->prepare("SELECT * FROM users WHERE username=?");
+ $stmt->execute([$_POST['username']]);
+ $userFromDataBase = $stmt->fetch();
+ /**
+ * Hydration
+ */
+ $user = (new \Test\DevCoder\Authentication\User())
+ ->setUserName($userFromDataBase['username'])
+ ->setPassword($userFromDataBase['password'])
+ ->setRoles(json_decode($userFromDataBase['roles']))
+ ->setEnabled($userFromDataBase['active']);
+
+ $userManager = new UserManager();
+ if ($userManager->isPasswordValid($user, $_POST['password'])) {
+
+ // login OK, set Token in session
+ $userManager->createUserToken($user);
+
+ } else {
+ // login failed , return error
+ }
diff --git a/classes/cache/Cache_interface.php b/classes/cache/Cache_interface.php
new file mode 100644
index 0000000..7c49789
--- /dev/null
+++ b/classes/cache/Cache_interface.php
@@ -0,0 +1,34 @@
+<?php
+
+/** Cache interface
+ * ---
+ * Interface to save results of expensive data-extraction proccess,
+ * in a fast-accessed medium (ussually RAM or NVME SSD units)
+ */
+interface Cache_interface
+{
+ /** Cache::set($key, $data, $ttl) : void
+ *
+ * @param $key (string): reference label
+ * @param $data (mixed): actual data (avoid storing true|false)
+ * @param $ttl (int): time to live (seconds)
+ */
+ public function set(string $key, $data, int $ttl);
+
+
+ /** Cache::get($key) : mixed
+ *
+ * @param $key (string)
+ * @return mixed: fetched $data -or- false on failure
+ */
+ public function get(string $key);
+
+
+ /** Cache::flush( $olderThan ) : void
+ *
+ * clear cache older than $olderThan
+ * @param $olderThan (int) in seconds
+ */
+ public function flush(int $olderThan = 0);
+
+} \ No newline at end of file
diff --git a/classes/cache/FileCache.php b/classes/cache/FileCache.php
new file mode 100644
index 0000000..fb7eee5
--- /dev/null
+++ b/classes/cache/FileCache.php
@@ -0,0 +1,93 @@
+<?php
+
+/* FileCache
+ * ---
+ * Saves serialized data into filesystem.
+ * Use it for expensive database-queries.
+ * Performanve on a NVMe-SSD drive is really great;
+ * (even better than MemCached and Redis)
+ * performance on a typical HDD is just fair.
+ *
+ * NOTE:
+ * Memcached and Redis are much faster cache-technologies
+ * and generally recommended; but they are not always available;
+ * FileCache is (almost) always available;
+ *
+ * Before tou decide on your caching technology
+ * run some benchmarks.
+ */
+class FileCache implements Cache_interface
+{
+
+ /** store
+ * ---
+ * serializes and saves data in a file
+ * along with TTL (time-to-live)
+ */
+ public function set(string $key, $data, int $ttl)
+ {
+ // filename is a hashed 48-hex-digit string
+ // probability of collision = exp( (-k*(k-1)) / 2N )
+ // ex: for 10tril.samples P(collision) = 1.5E-11%
+ $filename = FILECACHE_PATH . sha1($key);
+
+ // Opening file for write
+ $h = fopen($filename, 'w');
+ if (!$h) throw new Exception('Can not write to cache');
+
+ // Serialize along with the TTL
+ $data = serialize( array(
+ time() + $ttl, // array[0] holds expiration time
+ $data) // array[1] holds the actual data
+ );
+
+ if (fwrite($h,$data) === false) {
+ throw new Exception('Can not write to cache');
+ }
+ fclose($h);
+ }
+
+
+ /** fetch
+ * ---
+ * feth data for certain key
+ * @param $key (string)
+ * @return fetched $data -or- false on failure
+ */
+ public function get(string $key)
+ {
+ $filename = FILECACHE_PATH . sha1($key);
+
+ // can not read the cache-file? return false
+ if (!file_exists($filename) || !is_readable($filename)) return false;
+
+ $data = file_get_contents($filename); // get cache-file contents
+ $data = @unserialize($data); // unserialize
+
+ if (!$data) {
+
+ // Unlinking the file when unserializing failed
+ unlink($filename);
+ return false;
+
+ }
+
+ // checking if the data was expired
+ if (time() > $data[0]) {
+
+ // Unlinking
+ unlink($filename);
+ return false;
+
+ }
+
+ return $data[1];
+ }
+
+
+ public function flush(int $olderThan = 0)
+ {
+ return false; // reply false until implementation
+ }
+
+}
diff --git a/classes/cache/MemcachedCache.php b/classes/cache/MemcachedCache.php
new file mode 100644
index 0000000..c89d86a
--- /dev/null
+++ b/classes/cache/MemcachedCache.php
@@ -0,0 +1,37 @@
+<?php
+
+/** Memcached Implementation
+ * (implements Cache interface; extends Cache)
+ * ---
+ */
+class MemcachedCache implements Cache_interface
+{
+
+ public function set($key, $data, $ttl)
+ {
+ $hashkey = md5($key);
+
+ $mc = new Memcached();
+ $mc->addServer(\MEMCACHE_HOST, 11211);
+
+ $mc->set($hashkey, $data, $ttl);
+ }
+
+
+ public function get($key)
+ {
+ $hashkey = md5($key);
+
+ $mc = new Memcached();
+ $mc->addServer(\MEMCACHE_HOST, 11211);
+
+ return $mc->get($hashkey);
+
+ }
+
+ public function flush($olderThan = 0)
+ {
+ return false;
+ }
+
+} \ No newline at end of file
diff --git a/classes/cache/RedisCache.php b/classes/cache/RedisCache.php
new file mode 100644
index 0000000..ec50f08
--- /dev/null
+++ b/classes/cache/RedisCache.php
@@ -0,0 +1,69 @@
+<?php
+
+/** Redis Cache implementation
+ * ---
+ * This class uses \Predis\Redis, so do not forget require it via composer
+ * or to use the configuration options you will find into \README.md
+ */
+class RedisCache implements Cache_interface
+{
+ /** set
+ * store $data under the label $key
+ * cache expires in $ttl seconds
+ */
+ public function set($key, $data, $ttl)
+ {
+ $serialized = serialize($data);
+
+ if ($serialized = '') {
+ return false;
+ }
+
+ try {
+ $redis = new \Predis\Client([
+ 'host' => \REDIS_HOST
+ ]);
+ return $redis->set($key, $serialized, 'EX', $ttl);
+
+ } catch (Exception $e) {
+ // ...
+ }
+ }
+
+
+ /** get
+ * fetch previously stored $data under label $key
+ * return $data (or false)
+ */
+ public function get($key)
+ {
+
+ try {
+
+ $redis = new \Predis\Client([
+ 'host' => \REDIS_HOST
+ ]);
+
+ if ($redis->exists($key)) {
+ return unserialize($redis->get($key));
+
+ } else {
+ return false;
+ }
+
+ } catch (Exception $e) {
+ // ...
+ }
+
+ }
+
+
+ /** flush
+ * --- not umplemented yet; may not needed
+ */
+ public function flush($olderThan = 0)
+ {
+ return false; // until implementation
+ }
+
+} \ No newline at end of file
diff --git a/classes/session/DatabaseSession.php b/classes/session/DatabaseSession.php
new file mode 100644
index 0000000..aa31b62
--- /dev/null
+++ b/classes/session/DatabaseSession.php
@@ -0,0 +1,138 @@
+<?php
+
+// class DatabaseSession implements Session_interface
+class DatabaseSession implements SessionHandlerInterface
+{
+ /** PROPERTIES
+ * -------------------------------------------------------------------------
+ */
+
+ private $db; // the database object
+
+ // private $db_driver; // the database driver
+
+
+ /** METHODS
+ * -------------------------------------------------------------------------
+ */
+
+ /** __construct
+ * set database connection
+ * set session handler to overide default session
+ * start session
+ */
+ public function __construct()
+ {
+ // Prepare the Database object
+ $this->db = Registry::use('database');
+
+ // Set handler to overide SESSION
+ session_set_save_handler(
+ array($this, "open"),
+ array($this, "close"),
+ array($this, "read"),
+ array($this, "write"),
+ array($this, "destroy"),
+ array($this, "gc")
+ );
+
+ // Start the session
+ session_name(SESSION_NAME);
+ session_start();
+ }
+
+ // public function open(string $path, string $name) : bool
+ // NOTE: $path and $name are unsued
+ public function open() : bool
+ {
+ if ($this->db) {
+ return true;
+ }
+ else {
+ // database connection may be closed from another class
+ // (ex. from some user class); in this case...
+ // create a new database connection, then recheck.
+ $conn = new Database();
+ // $this->db = $conn->connect();
+
+ if ($conn) {
+ $this->db = $conn;
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ public function close() : bool
+ {
+ // depricated: the database object is shared (don;t clode it!)
+ // // Close the database connection
+ // if ($this->db = null) { return true; }
+ // else return false;
+
+ return true;
+ }
+
+
+ public function read(string $id) : string|false
+ {
+ $exist = $this->db->runQuery(
+ "SELECT data FROM sessions WHERE id = :id",
+ [':id', $id]
+ );
+
+ if (($exist === false) || (count($exist) == 0)) {
+ return false;
+
+ } else {
+ $data = $exist[0];
+ }
+
+ if (is_null($row['data'])) {
+ return '';
+ }
+ return $row['data'];
+ }
+
+
+ public function write(string $id, string $data) : bool
+ {
+ // Create timestamp
+ $access = time();
+ $check = $this->db->runQuery(
+ "REPLACE INTO sessions VALUES (:id, :access, :data)",
+ [':id' => $id, ':access' => $access, ':data' => $data ]
+ );
+ return ($check == false) ? false : true;
+ }
+
+
+ public function destroy(string $sassionID) : bool
+ {
+ $check = $this->db->runQuery(
+ 'DELETE FROM sessions WHERE id = :id',
+ [':id' => $sassionID]
+ );
+ return ($check == false) ? false : true;
+ }
+
+
+ public function gc(int $max)
+ {
+ // Calculate what is to be deemed old
+ $old = time() - $max;
+ $check = $this->db->runQuery(
+ 'DELETE FROM sessions WHERE access < :old',
+ [':old' => $old]
+ );
+ return ($check == false) ? false : 1;
+ // check garbage-collector probability to run
+ // echo "probability: ". ini_get("session.gc_probability") ." / ". ini_get("session.gc_divisor") . ", ttl: ". ini_get("session.gc_maxlifetime"); die();
+
+ }
+
+ # callable $create_sid = ?,
+ # callable $validate_sid = ?,
+ # callable $update_timestamp = ?
+}
diff --git a/classes/session/DefaultSession.php b/classes/session/DefaultSession.php
new file mode 100644
index 0000000..d3099a5
--- /dev/null
+++ b/classes/session/DefaultSession.php
@@ -0,0 +1,35 @@
+<?php
+
+/** DefauleSession
+ * --
+ * is a dummy session hanlder that wraps the
+ * php's default session engine implementation;
+ */
+class DefaultSession implements SessionHandlerInterface {
+
+ public function __construct()
+ {
+ // Start the session
+ session_name(SESSION_NAME);
+ session_start();
+ }
+
+ #[\ReturnTypeWillChange]
+ public function open(string $path, string $name) {}
+
+ #[ReturnTypeWillChange]
+ public function close() {}
+
+ #[ReturnTypeWillChange]
+ public function read(string $id) {}
+
+ #[ReturnTypeWillChange]
+ public function write(string $id, string $data) {}
+
+ #[ReturnTypeWillChange]
+ public function destroy(string $id) {}
+
+ #[ReturnTypeWillChange]
+ public function gc(int $max_lifetime) {}
+
+} \ No newline at end of file
diff --git a/classes/session/FilesSession.php b/classes/session/FilesSession.php
new file mode 100644
index 0000000..8762776
--- /dev/null
+++ b/classes/session/FilesSession.php
@@ -0,0 +1,142 @@
+<?php
+
+/** FileSessionHandler
+ * is a custom File-based Session Handler
+ *
+ * Could be useful when implemented along with data-cryptography,
+ * otherwise php's default session handler (which is also file-based)
+ * seems to be the obvious way to go;
+ *
+ * NOTE:
+ * if shared sessions accros an array of servers is needed,
+ * a database-session handler is probably the best choice.
+ *
+ * TODO:
+ * implement cryptography
+ */
+class FileSessionHandler implements SessionHandlerInterface
+{
+ /** The filesystem instance. */
+ protected $sessionName;
+
+ /** The path where all sessions should be stored. */
+ protected $path;
+
+ /** The number of minutes the session should be valid. */
+ protected $minutes;
+
+ /**
+ * Create a new file driven session-handler instance.
+ *
+ * @param string $path
+ * @param int $minutes
+ * @return void
+ */
+ public function __construct()
+ {
+ // values comming from configuration
+ $this->sess_filename = $path;
+ $this->minutes = $minutes;
+
+ // Set handler to overide SESSION
+ session_set_save_handler(
+ array($this, "open"),
+ array($this, "close"),
+ array($this, "read"),
+ array($this, "write"),
+ array($this, "destroy"),
+ array($this, "gc")
+ );
+
+ // Start the session
+ session_name(SESSION_NAME);
+ session_start();
+ }
+
+ /** open
+ * set filename;
+ * no need to touch the filesystem yet
+ * return true (always)
+ */
+ public function open($savePath, $sessionName): bool
+ {
+ $this->sess_filename = $this->$path .'/'. $sessionName;
+ return true;
+ }
+
+ /** close
+ * nothing needs to be closed;
+ * return true (always)
+ */
+ public function close(): bool
+ {
+ return true;
+ }
+
+ /** read
+ * chech if file exists; if not return false
+ * read data
+ */
+ public function read($sessionId): string|false
+ {
+ if (!file_exists($filename) || !is_readable($filename)) return false;
+ return file_get_contents($filename);
+
+ # if ($this->files->isFile($path = $this->path.'/'.$sessionId) &&
+ # $this->files->lastModified($path) >= Carbon::now()->subMinutes($this->minutes)->getTimestamp()) {
+ # return $this->files->sharedGet($path);
+ # }
+
+ # depricated? return '';
+ }
+
+ /** write
+ * data serielized already by php's internal session engine
+ */
+ public function write($sessionId, $data): bool
+ {
+ $h = fopen($filename, 'w');
+ if (fwrite($h,$data) === false) {
+ throw new Exception('Could not write session data');
+ return false;
+ }
+ fclose($h);
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return bool
+ */
+ public function destroy($sassionID): bool
+ {
+ unlink($this->sassionID);
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return int
+ */
+ public function gc($lifetime) : int
+ {
+ $files = Finder::create()
+ ->in($this->path)
+ ->files()
+ ->ignoreDotFiles(true)
+ ->date('<= now - '.$lifetime.' seconds');
+
+ $deletedSessions = 0;
+
+ foreach ($files as $file) {
+ $this->files->delete($file->getRealPath());
+ $deletedSessions++;
+ }
+
+ return $deletedSessions;
+ }
+}
diff --git a/classes/session/SessionHandlerInterface.php b/classes/session/SessionHandlerInterface.php
new file mode 100644
index 0000000..cf6efac
--- /dev/null
+++ b/classes/session/SessionHandlerInterface.php
@@ -0,0 +1,169 @@
+<?php
+
+/** SessionHandlerInterface
+ *
+ * as of php's documentation (check manual)
+ * https://www.php.net/manual/en/class.sessionhandlerinterface.php
+ */
+interface SessionHandlerInterface
+{
+
+ /* Methods
+ *--------------------------------------------------------------------------
+ */
+
+ public function open(string $path, string $name) : bool;
+
+ public function close() : bool;
+
+ public function read(string $id) : string|false;
+
+ public function write(string $id, string $data) : bool;
+
+ public function destroy(string $id) : bool;
+
+ public function gc(int $max_lifetime) : int|false;
+
+}
+
+/** TODO: (thoughts)
+ * ... for a brand-new session implementation
+ *
+ * In most cases, the session data does not require persistence.
+ * The session information could be cached to improve performance
+ *
+ * So (in theory) we could create a two-dimension session mechanism
+ *
+ * + a fast dimension
+ * : could be file-based (using local FS [php's default?] or redis/memcached)
+ *
+ * + a shared dimension
+ * : implemented in a shared database
+ *
+ * Posible algo:
+ * ... (check later on this comment-block: session life-cycle )
+ *
+ * open :
+ * close : on both
+ * read : check Local; if not exist check database; set session_id($id)
+ * write : on both
+ * destroy : on both
+ * gc : on server
+ *
+ * when regenerating session_id, keep database informed about the new session_id
+ * when closing, keep database informed about the session end-of-life.
+ *
+ * a session timeout on the local leg will trigger a confirm-session-from-db
+ *
+ * garbage collector on the db should remove expired sessions of some X seconds
+ * and earier (X needs to be determined by practice/expirience/tries)
+ *
+ * (+) in order to keep tracking of the session between multiple servers through
+ * the (shared) database, a unique secret-between-the-servers id can be used
+ *
+ * so...
+ *
+ * #1
+ * -> someone visits website through server A
+ * -> server A creates a new session-id for the visitor and a shared-session-id
+ * -> saves both into database
+ *
+ * later...
+ *
+ * #2
+ * -> the same device is connected (via loadbalancer) into server B
+ * -> server B don't have the session-id (send by the client) but gets it from db
+ * (is session-id do not exist on the db, then this is a new session)
+ *
+ * = now both server A and B have the same session-id localy
+ *
+ * later...
+ *
+ * #3
+ * -> some server (let's say B) regenerates the visitors session-id
+ * -> if later the visitor falls into server A, the A will retrieve the new
+ * session id through the #2 scenario
+ *
+ * this way
+ * + any server can update/regenerate the visitor's session-id
+ * + the visitor can be served from both servers randomly
+ * + all session variables exist on the database (always)
+ * + all session variables exist on all servers synced-on-demand
+ *
+ *
+ * stucture of session:
+ * ---
+ * - shared-session-id: (secret + persistent); exposed between servers **primary
+ * - session-id: (may change/regenerate/update); exposed to the client **indexed
+ * - data: serialized array
+ * - creation_timestamp:
+ * - last_touched_timestamp:
+ * - expiration_timestamp: should point to the future or session has expired
+ * - csrf_token:
+ * - JWT_token
+ * - AES-key (this way private data can be kept in browser)
+ *
+ *
+ * what will kept on browser/client (via cookie)
+ * ---
+ * - session-name => session-id
+ * - user info => AES_ectypted(serialized[t=>csfr_token, u=> user_id, s=>SIGNATURE])
+ *
+ *
+ * Writable file-system shall change
+ * (local writable file-system tree)
+ * ---
+ * /html/storage
+ * |
+ * |-- cache : query-caches
+ * |
+ * `-- session : FS\sessions
+ * |
+ * `-- indexes : share-session-id indexes
+ *
+ *
+ *
+ * NOTE: Session life-cycle
+ *
+ * session_start() * firsts time
+ * ---
+ * ::open(path,PHPSESSID) -> (session_id not exist) -> false
+ * ::create_sid -> '123def'
+ * ::read('123def') ?-or/and- ::close()
+ *
+ *
+ * session_start() * next times
+ * ---
+ * ::open(path, PHPSESSID) -> (session_id exist) -> true
+ * ::read('123def') -> return data -> will fill $_SESSION[*]
+ *
+ *
+ * $_SESSION['foo'] = 'bar';
+ * ---
+ * ::write('123def', 'foo|s:3:"bar";') -> ['foo' => "bar"]
+ * ::close()
+ *
+ *
+ * session_regenerate_id();
+ * ---
+ * ::create_sid() -> def123
+ *
+ *
+ * session_reset()
+ * ---
+ * ::open()
+ * ::read('def123')
+ *
+ *
+ * session_write_close()
+ * ---
+ * ::write('123def', 'foo|s:3:"bar";')
+ * ::close()
+ *
+ *
+ * session_destroy()
+ * ---
+ * ::destroy('def123')
+ * ::close()
+ *
+ */