summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-04-27 03:47:30 +0300
committerGeorge Halkiadakis <gchalkiadakis@sklavenitis.co.gr>2023-04-27 03:47:30 +0300
commit059e0d95d0c28bc5060e87e146eaf7411f51bf90 (patch)
tree7c21ac432f16dde2329a0d5954d70711eceb48e6 /core
parent26cd8ee99659ef1926c96a049c93645ffc9b169d (diff)
downloadgyraf1gov-059e0d95d0c28bc5060e87e146eaf7411f51bf90.tar.gz
gyraf1gov-059e0d95d0c28bc5060e87e146eaf7411f51bf90.tar.bz2
gyraf1gov-059e0d95d0c28bc5060e87e146eaf7411f51bf90.zip
skeleton commit; based on an anom project
Diffstat (limited to 'core')
-rw-r--r--core/README.md266
-rw-r--r--core/auth/.gitkeep0
-rw-r--r--core/auth/env.example36
-rw-r--r--core/classes/Benchmark.php150
-rw-r--r--core/classes/Cart.php71
-rw-r--r--core/classes/Database.php333
-rw-r--r--core/classes/Registry.php135
-rw-r--r--core/classes/Render.php358
-rw-r--r--core/classes/Repository.php43
-rw-r--r--core/classes/Request.php161
-rw-r--r--core/classes/Route.php97
-rw-r--r--core/classes/Security.php923
-rw-r--r--core/classes/authentication/Core/PasswordTrait.php38
-rw-r--r--core/classes/authentication/Core/UserManager.php101
-rw-r--r--core/classes/authentication/Core/UserManagerInterface.php25
-rw-r--r--core/classes/authentication/Token/UserToken.php44
-rw-r--r--core/classes/authentication/Token/UserTokenInterface.php18
-rw-r--r--core/classes/authentication/User.php116
-rw-r--r--core/classes/authentication/UserInterface.php19
-rw-r--r--core/classes/authentication/info.md114
-rw-r--r--core/classes/cache/Cache_interface.php34
-rw-r--r--core/classes/cache/FileCache.php93
-rw-r--r--core/classes/cache/MemcachedCache.php37
-rw-r--r--core/classes/cache/RedisCache.php69
-rw-r--r--core/classes/session/DatabaseSession.php138
-rw-r--r--core/classes/session/DefaultSession.php42
-rw-r--r--core/classes/session/FilesSession.php138
-rw-r--r--core/classes/session/SessionHandlerInterface.php169
-rw-r--r--core/cli/cli-cache.php65
-rwxr-xr-xcore/cli/curl-cache.php155
-rw-r--r--core/cli/info.md23
-rw-r--r--core/cli/micro/setup.php120
-rw-r--r--core/cli/micro/term_utilities.php44
-rw-r--r--core/config/anom_settings.php325
-rw-r--r--core/config/constants.php28
-rw-r--r--core/config/init.php64
-rw-r--r--core/helpers/autoload.php43
-rw-r--r--core/helpers/design_patterns.php104
-rw-r--r--core/helpers/error_handling.php119
39 files changed, 4858 insertions, 0 deletions
diff --git a/core/README.md b/core/README.md
new file mode 100644
index 0000000..8dee940
--- /dev/null
+++ b/core/README.md
@@ -0,0 +1,266 @@
+## x-anom
+### X is ANOther Mvc
+
+x-anom is an Object-Oriented MVC php-framework.
+
+Main advantages of the framework:
+
+* It is super-light and fast;
+* Core has almost zero dependences and contains almost anything you need to start and optimize a server-based web-application.
+* It is extensible especialy if you use Composer (which is recommended, though not required)
+* Handles security; and as long as you write code using secure practices, it will be secure
+* It's easy to configure, easy to code, easy to use
+
+
+
+### Requirements
+
+* PHP 8+
+* Some webserver (Apache/Nginx)
+* Some SQL server (MySQL/MariaDB/Postgress)
+* Basic knowledge of php and SQL
+
+
+
+
+### What is included
+
+Core components:
+
+
+#### Router
+
+Routes request to the appropriate Controller
+- matches static paths
+- matches dynamic paths through regex expressions
+- takes care of request-method
+
+
+#### Controller and Model
+
+(just write your Controller and Model classes)
+
+
+#### View (rendering engine)
+
+Usually you just need to call the render_view(template, data) function.
+For templating we use the php short-tag syntax.
+
+main functions:
+
+* load_template( template, data)
+
+* render_view( view_file , data )
+
+* load_asset( type, assets_array )
+
+* set_headers( content_type, ttl, more_array )
+
+* render_text( data, content_type, ttl )
+
+* reply_json( data, ttl )
+
+
+#### Class autoloader
+
+Composer is recomende;
+but if is not available this one will do the job
+
+
+#### Caching
+
+* File Caching mechanism
+
+* Redis Cache
+
+* Memcached Caching
+
+
+#### Session Management
+
+* DefaultSession: psevdo-handler with passthrough methods
+
+* FileSession: custom file-based session handler
+
+* DatabaseSession: database session handler
+
+
+
+#### More
+
+* Proxy design pattern
+
+* Error handling
+
+* Repository
+
+* Cli interface (*summarizes anom*)
+
+
+
+
+
+### Other features
+
+* Docker ready
+
+You can run the framework as is in a Docker environment;
+Also. you can edit just a bit the configuration files to customize the project
+to support various technologies; Check the README file in the project's root
+folder to find out more.
+
+
+
+### What is not included
+
+* It lacks a query builder (SQL is easy and very powerful); do not forget to use prepared statements for all your SQL queries.
+
+* TODO: User-Role based administration class.
+
+* TODO: Shoping-Cart class.
+
+
+
+## Life (and death) of a Client_Request–Server_reply session
+
+1. THE REQUEST: client makes a request -> request arives to the server -> .htaccess sends the request to the index.php
+
+2. index loads Configuration (constants.php + config.php) and AUTOLOADER in order load classes easily
+
+3. index loads init.php -> after initialization the APP is READY -> index loads the routes; Route::run() -> the APP is RUNNING
+
+4. Router resolves the request pattern and calls a Controller
+
+5. (if needed) Controller asks data from the Model; then responds a reply to the client using **render** functions -> APP dies
+
+
+
+## Direcrtory structure
+
+NOTE: directory structure needs updata, but the main structure ramains untouched.
+
+ .
+ |-- container * for docker/container configuration
+ | |-- bin * scripts
+ | `-- config * settings
+ |
+ |-- core * core code
+ | |-- auth * authentication and credentials
+ | |
+ | |-- config * application parametres
+ | | |-- config.php
+ | | |-- constants.php
+ | | |-- credentials.php
+ | | `-- init.php
+ | |
+ | |-- classes * core classes
+ | | |-- cacher
+ | | |-- Benchmark.php
+ | | |-- Database.php
+ | | |-- Route.php
+ | | |-- Security.php
+ | | `-- Session.php
+ | |
+ | `-- helpers * core helpers
+ | |-- autoload.php
+ | |-- error-handling.php
+ | `-- render.php
+ |
+ |-- data * folder for batch data-imports to database
+ |
+ |-- public ** PUBLIC directory
+ | |-- app * APP
+ | | |-- Controllers * Controllers
+ | | | |-- Art.php
+ | | | `-- ...
+ | | |
+ | | |-- Models * Models
+ | | | |-- Art_model.php
+ | | | `-- ...
+ | | |
+ | | |-- Views * Views
+ | | | |-- group.php
+ | | | `-- item.php
+ | | |
+ | | `-- routes.php * application routes
+ | |
+ | `-- cache * Caching folder
+ |
+ `-- vendor * Vendor classes and autoloader
+ |-- composer
+ `-- ...
+
+
+
+
+## Naming Conventions and good practices
+
+1. Keep Controllers, Models, Views in their folders
+
+2. Organize View elements in subfolders
+
+3. Controller and Model names shall be camelcased;
+ - fist letter should be Uppercase;
+ - classes shall be named exactly as their filenames
+
+4. Model names shall be suffixed with _model
+
+5. Comment every-single Controller/Model/class and comment every method
+
+6. On Views (php templates) use php short-tags when possible
+ - Views are about rendering data; avoid complex-logic
+ - [if/else], [foreach] and some flag/temp [variables] sould be fair enough
+
+7. All of the above rules are strongly recommended (although not obligatory);
+
+
+
+## Notes and brainstorming
+
+### for template system check
+ https://css-tricks.com/php-is-a-ok-for-templating/
+
+### for rendering system you may check volt too
+ https://docs.phalcon.io/4.0/en/volt
+
+
+
+## Brainstorming
+
+* take care of various hacks
+ chk: https://stackoverflow.com/questions/1996122/how-to-prevent-xss-with-html-php
+
+* HTML to MarkDown!
+ chk: https://github.com/thephpleague/html-to-markdown
+
+
+
+
+
+VIEW data
+
+-> ceo -> title
+ -> description
+ -> keywords
+ -> ...
+
+-> content -> view : view_filename
+ -> key : some_variable_name
+ -> data : the_data
+
+
+
+
+## Knowledge Requirements
+
+## Tools of work
+
+None of the following tools is necessary, but they will help very very-much.
+
+- Composer
+
+- Docker
+
+- Code editor
+
+- Coffee + Nicotine
diff --git a/core/auth/.gitkeep b/core/auth/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/core/auth/.gitkeep
diff --git a/core/auth/env.example b/core/auth/env.example
new file mode 100644
index 0000000..fd47dd8
--- /dev/null
+++ b/core/auth/env.example
@@ -0,0 +1,36 @@
+ENVIRONMENT='development'
+
+# MySQL connection
+# --- -- -- - - -
+DB_NAME=
+DB_USER=
+DB_PASS=
+PDO_HOST=
+
+# mailhog
+# == testting/fake email service
+# --- -- -- - - -
+# MAIL_HOST=mailhog_server
+# MAIL_PORT=1025
+# MAIL_USERNAME=
+# MAIL_PASSWORD=
+# MAIL_ENCRYPTION=
+
+# SMTP setup
+# == read mail server
+# --- -- -- - - -
+MAIL_MAILER=smtp
+MAIL_HOST=
+MAIL_PORT=465
+MAIL_USERNAME=
+MAIL_PASSWORD=
+MAIL_ENCRYPTION=ssl
+NO_REPLY_EMAIL=noreply@...
+REPLY_TO_EMAIL=webmaster@r...
+MAIL_FROM_NAME=Classroom
+
+# redis cache server
+# --- -- -- - - -
+# REDIS_HOST=redis
+# REDIS_PASSWORD=
+# REDIS_PORT=6379 \ No newline at end of file
diff --git a/core/classes/Benchmark.php b/core/classes/Benchmark.php
new file mode 100644
index 0000000..a7e93ea
--- /dev/null
+++ b/core/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/core/classes/Cart.php b/core/classes/Cart.php
new file mode 100644
index 0000000..4e6f585
--- /dev/null
+++ b/core/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/core/classes/Database.php b/core/classes/Database.php
new file mode 100644
index 0000000..8cac479
--- /dev/null
+++ b/core/classes/Database.php
@@ -0,0 +1,333 @@
+<?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->lastInsertId();
+ }
+
+
+ /** 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/core/classes/Registry.php b/core/classes/Registry.php
new file mode 100644
index 0000000..047fd54
--- /dev/null
+++ b/core/classes/Registry.php
@@ -0,0 +1,135 @@
+<?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.
+ *
+ * It implements the Facade design pattern;
+ * it defines a new interface for existing objects.
+ *
+ * ---
+ */
+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/core/classes/Render.php b/core/classes/Render.php
new file mode 100644
index 0000000..3b68c26
--- /dev/null
+++ b/core/classes/Render.php
@@ -0,0 +1,358 @@
+<?php
+/** Rendering System
+ *
+ * this is the View part of the anom MVC framework
+ ***/
+
+ Class Render {
+
+ /** load template
+ * ---
+ * TODO:
+ * explain/document the difference between a template and a view
+ *
+ * TODO:
+ * Implementing the Render/View operation as a class, using the same-
+ * name methods with Laraver or CodeIgniter could be good idea;
+ * and get rid of if(!defined('OUTPUT_STARTED')) define('OUTPUT_STARTED',1);
+ */
+ public static function template($template, $data) {
+
+ $file = VIEWS_DIRECTORY . $template . '.php';
+
+ if (file_exists($file)) {
+ if (!defined('OUTPUT_STARTED')) define('OUTPUT_STARTED', 1);
+
+ ob_start();
+
+ extract( self::sanitize_output($data) );
+ require( $file );
+
+ ob_flush();
+
+ } else if (!PRODUCTION) {
+
+ echo "<!-- view ". $file ." is missing -->";
+ }
+
+ }
+
+ /** parse_sections
+ * ---
+ * parse a group of views (sections)
+ * @param $sections: an array of sections
+ *
+ * each section group is an array with view, key and data properties
+ * + view: defines the view template/file
+ * + key: the variable name that view uses to parse data OR empty-string*
+ * * if key is an empty then $data should be an array (which
+ * includes all [variable-name:data] pairs utilized by the view)
+ * + data: holds the actual data
+ */
+
+ public static function sections($sections) {
+
+ foreach($sections as $sect) {
+
+ if ($sect['key'] == '') {
+ self::view( $sect['view'], $sect['data'] );
+
+ } else {
+ self::view( $sect['view'], [ $sect['key'] => $sect['data']] );
+ }
+ }
+ }
+
+
+ /** render function
+ * ---
+ * uses php's short-tag syntax for templating system
+ * extract data into template
+ * @param $view: view-template filename
+ * @param $data: data to embed into view-template
+ * @param $sanitize: of true then sanitize data.
+ * important NOTE: data is an array [key => value]
+ */
+ public static function view($view, $data=[], $sanitize = false) {
+
+ $file = VIEWS_DIRECTORY . $view . '.php';
+
+ if (file_exists($file)) {
+ if (!defined('OUTPUT_STARTED')) define('OUTPUT_STARTED', 1);
+
+ extract( $sanitize ? self::sanitize_output($data) : $data );
+ require( $file );
+
+ } else if (!PRODUCTION) {
+
+ echo "<!-- view ". $file ." is missing -->";
+ }
+
+ }
+
+
+ /** render asap
+ * ---
+ * render_view then output code
+ * so that client will get html to render
+ * while server calculates next html
+ */
+ public static function asap($view, $data, $sanitize = false) {
+ self::view($view, $data, $sanitize);
+ ob_flush();
+ }
+
+
+ /** render text
+ * ---
+ * This function simply echoes text
+ * with Content-Type and Cache Headers
+ * @param $data : the text to echo
+ * @param $cType : Content-Type header
+ * @param $ttl : cache-contol headers; if false response is not-cached; else (int) chache for $ttl seconds
+ */
+ public static function text( $data, $contentType = 'text/html; charset=UTF-8', $ttl = false ) {
+ header('Content-Type: '. $contentType);
+ if ($ttl) {
+ $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT";
+ header("Expires: {$ts}");
+ header("Pragma: cache");
+ header("Cache-Control: max-age={$ttl}");
+
+ } else {
+ $ts = gmdate("D, d M Y H:i:s") . " GMT";
+ header("Expires: {$ts}");
+ header("Last-Modified: {$ts}");
+ header("Pragma: no-cache");
+ header("Cache-Control: no-cache, must-revalidate");
+ }
+ echo htmlspecialchars($data);
+ }
+
+
+ /** reply_json
+ * ---
+ * transform a php-array to json and echo to client
+ * Can be used for API calls
+ *
+ * @param $data : the php array)
+ * @param $ttl : cache-contol headers; if false response is not-cached; else chache for $ttl seconds
+ */
+ public static function json( $data, $ttl = false ) {
+ header('Content-Type: application/json');
+ if ($ttl) {
+ $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT";
+ header("Expires: {$ts}");
+ header("Pragma: cache");
+ header("Cache-Control: max-age={$ttl}");
+
+ } else {
+ $ts = gmdate("D, d M Y H:i:s") . " GMT";
+ header("Expires: {$ts}");
+ header("Last-Modified: {$ts}");
+ header("Pragma: no-cache");
+ header("Cache-Control: no-cache, must-revalidate");
+ }
+ echo json_encode($data, JSON_UNESCAPED_UNICODE);
+
+ // NOTE:
+ // after Rendering a JSON ...
+ // you probably do not need to export anything else;
+ die();
+ }
+
+
+
+
+ // HELPER FUNCTIONS
+ // -----------------------------------------------------------------------------
+
+
+ /** link asset
+ * ---
+ * the function defines the assets (css or js)
+ * to be loaded on the client (creates the HTML)
+ * @param $type : type of asset [css|js]
+ * @param $assetIDs : an array of ('assetID' => 'paramaters')
+ *
+ * example calls:
+ * load_asset('css', ['main' => 'media="all"', 'filters' => 'media="all"']);
+ * load_asset( 'js', ['jquery' => '', 'lazyloader' => 'async']);
+ */
+ public static function asset( $type, $assetIDs, $paramatres = '' ) {
+
+ $code = ""; // code to return
+
+ // make sure $assetIDs is array (for coding simplicity)
+ if (!is_array($assetIDs)) {
+ $assetIDs = [ $assetIDs ];
+ }
+
+ // check asset type and construct all asset inserts
+ switch ($type) {
+
+ case 'font':
+ foreach($assetIDs as $asset) {
+ $code .= "\n\t<link href='". FONTS_DIR . FONT_FILES[$asset] ."' as='font' type='font/woff2' {$paramatres}/>";
+ }
+ break;
+
+ case 'js':
+ foreach($assetIDs as $asset) {
+ $code .= "\n\t<script src='". JS_DIR . JS_LIBRARIES[$asset] ."' id='{$asset}' {$paramatres}></script>";
+ }
+ break;
+
+ case 'css':
+ default:
+ foreach($assetIDs as $asset) {
+ $code .= "\n\t<link rel='stylesheet' href='". CSS_DIR . CSS_FILES[$asset] ."' id='{$asset}' {$paramatres} />";
+ }
+ }
+
+ echo $code;
+ }
+
+
+ /** sanitize_output (recursive)
+ * ---
+ * Sanitizes data that are about to rendered
+ * (usually when render_view() is called).
+ *
+ * NOTE:
+ * mitigates XSS attachs
+ *
+ * @param $data: (array)
+ */
+ public static function sanitize_output($data) {
+ //// check https://stackoverflow.com/questions/2002710/php-how-to-perform-htmlspecialchar-on-an-array-of-arrays
+
+ //// $output = array_map("myFunc", $data);
+
+ global $secure;
+
+ $output = array();
+ foreach($data as $key => $val) {
+
+ if (is_string($val)) {
+ $output[$key] = htmlspecialchars(self::remove_invisible_characters($val));
+
+ } else if (is_array($val)) {
+ $output[$key] = self::sanitize_output($val);
+
+ } else {
+ $output[$key] = $val;
+ }
+ }
+ return $output;
+ }
+
+
+ /** remove_invisible_characters()
+ * ---
+ * @used by sanitize_output()
+ */
+ public static function remove_invisible_characters($str, $url_encoded = TRUE)
+ {
+ $non_displayables = array();
+
+ // every control character except newline (dec 10),
+ // carriage return (dec 13) and horizontal tab (dec 09)
+ if ($url_encoded) {
+ $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15
+ $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31
+ $non_displayables[] = '/%7f/i'; // url encoded 127
+ }
+
+ $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127
+
+ do {
+ $str = preg_replace($non_displayables, '', $str, -1, $count);
+ } while ($count);
+
+ return $str;
+ }
+
+
+ /** html
+ * ---
+ * outputs code as html
+ *
+ * @param $str (string)
+ * @return html5 (string)
+ */
+ public static function html($str) {
+ if (!isset($str) || $str== null) return;
+ if (($str == '') || is_numeric($str)) return $str;
+ return htmlspecialchars_decode($str, ENT_QUOTES|ENT_HTML5);
+ }
+
+
+ /** set_headers
+ * ---
+ * set custom response-Headers
+ *
+ * @param $contentType
+ * @param $ttl: int or false)
+ * @param $more: array of (content-type => content-value) pairs
+ */
+ public static function set_headers($contentType = 'text/html; charset=UTF-8', $ttl = false, $more = [] ) {
+
+ // handle common content-type shorcuts
+ switch ($contentType) {
+ case 'text':
+ case 'html':
+ $contentType = 'text/html; charset=UTF-8';
+ break;
+ case 'json':
+ $contentType = 'application/json; charset=utf-8';
+ break;
+ case 'js':
+ $contentType = 'application/javascript; charset=utf-8';
+ break;
+ case 'css':
+ $contentType = 'text/css';
+ break;
+ default:
+ // $contentType stays as-is
+ break;
+ }
+
+ // send headers
+ header('Content-Type: '. $contentType);
+ if ($ttl) {
+ $ts = gmdate("D, d M Y H:i:s", time() + $ttl) . " GMT";
+ header("Expires: {$ts}");
+ header("Pragma: cache");
+ header("Cache-Control: max-age={$ttl}");
+
+ } else {
+ $ts = gmdate("D, d M Y H:i:s") . " GMT";
+ header("Expires: {$ts}");
+ header("Last-Modified: {$ts}");
+ header("Pragma: no-cache");
+ header("Cache-Control: no-cache, must-revalidate");
+ }
+
+ // send more headers
+ if ($more != []) {
+ foreach($more as $header => $value) {
+ header($header .': '. $value);
+ }
+ }
+
+ }
+
+
+ /** render file
+ *
+ */
+ public static function file($path, $madia_type)
+ {
+ $content = file_get_contents($path);
+ header("Content-Type: {$media_type}");
+ echo $content;
+ }
+
+
+} \ No newline at end of file
diff --git a/core/classes/Repository.php b/core/classes/Repository.php
new file mode 100644
index 0000000..c01d0c1
--- /dev/null
+++ b/core/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/core/classes/Request.php b/core/classes/Request.php
new file mode 100644
index 0000000..89928f9
--- /dev/null
+++ b/core/classes/Request.php
@@ -0,0 +1,161 @@
+<?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 = []; // $_GET array
+
+ public $POST = []; // $_POST array
+
+ public $FILES = []; // $_FILES array
+
+ 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->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->FILES = $_FILES; // TODO: sanitize the files array
+
+ // $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();
+ }
+
+ }
+
+
+ /**
+ * Check:
+ *
+ * 1.
+ * https://dev.to/anastasionico/good-practices-how-to-sanitize-validate-and-escape-in-php-3-methods-139b
+ *
+ * 2.
+ * https://benhoyt.com/writings/dont-sanitize-do-escape/
+ *
+ */
+ private function sanitize($array)
+ {
+ // TODO:
+ // ...
+ return $array;
+ }
+
+
+ public function checkCsrfToken()
+ {
+ // TODO:
+ // ...
+ // if SCRF-token is not valideted, serve 403
+ return true;
+ }
+
+
+
+
+ public function isAjax(): bool
+ {
+ // check headers 'XMLHttpRequest' == $this->headers->get('X-Requested-With');
+ }
+
+
+ public function isSecure(): bool
+ {
+ // chech if HTTPS
+ }
+
+
+ public function hasSession(): bool
+ {
+ // chech if Session exist
+ }
+
+
+ /**
+ * Get the user making the request.
+ *
+ * @param string|null $guard
+ * @return mixed
+ */
+ public function user($guard = null)
+ {
+ // return call_user_func($this->getUserResolver(), $guard);
+ }
+
+ public function getUserResolver()
+ {
+ // return $this->userResolver ?: function () {
+ //
+ // };
+ }
+
+ /**
+ * Set the user resolver callback.
+ *
+ * @param \Closure $callback
+ * @return $this
+ */
+ public function setUserResolver(Closure $callback)
+ {
+ // $this->userResolver = $callback;
+ // return $this;
+ }
+
+
+
+}
+
diff --git a/core/classes/Route.php b/core/classes/Route.php
new file mode 100644
index 0000000..072723c
--- /dev/null
+++ b/core/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/core/classes/Security.php b/core/classes/Security.php
new file mode 100644
index 0000000..61979db
--- /dev/null
+++ b/core/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/core/classes/authentication/Core/PasswordTrait.php b/core/classes/authentication/Core/PasswordTrait.php
new file mode 100644
index 0000000..40a7381
--- /dev/null
+++ b/core/classes/authentication/Core/PasswordTrait.php
@@ -0,0 +1,38 @@
+<?php
+/** Trait PasswordTrait
+ *
+ * @package DevCoder\Authentication\Core
+ *
+ */
+# namespace DevCoder\Authentication\Core;
+#
+# use DevCoder\Authentication\UserInterface;
+
+trait PasswordTrait
+{
+
+ private $cost = 10; // Cost must be in the range of 4-31
+ // a cost value in the range of 8-11
+ // is a good balance between performance and security
+
+
+ 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 > 31) {
+ throw new \InvalidArgumentException('Cost must be in the range of 4-31.');
+ }
+ $this->cost = $cost;
+ }
+}
diff --git a/core/classes/authentication/Core/UserManager.php b/core/classes/authentication/Core/UserManager.php
new file mode 100644
index 0000000..54cd2cb
--- /dev/null
+++ b/core/classes/authentication/Core/UserManager.php
@@ -0,0 +1,101 @@
+<?php
+/** Class UserManager
+ *
+ * @package DevCoder\Authentication\Core
+ *
+ */
+# namespace DevCoder\Authentication\Core;
+#
+# use DevCoder\Authentication\Token\UserToken;
+# use DevCoder\Authentication\Token\UserTokenInterface;
+# use DevCoder\Authentication\UserInterface;
+
+class UserManager implements UserManagerInterface
+{
+
+ use PasswordTrait;
+
+
+ public function __construct()
+ {
+ if (session_status() === PHP_SESSION_NONE) {
+ session_start();
+ }
+ }
+
+
+ /** getUserToken()
+ * == get user from session
+ *
+ */
+ public function getUserToken(): ?UserTokenInterface
+ {
+ $userToken = null;
+ if ($this->hasUserToken()) {
+ $userToken = unserialize($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
+ }
+
+ return $userToken;
+ }
+
+
+ /** hasUserToken()
+ * == is user loged in
+ *
+ */
+ public function hasUserToken(): bool
+ {
+ $key = UserTokenInterface::DEFAULT_PREFIX_KEY;
+ return (array_key_exists($key, $_SESSION) && unserialize($_SESSION[$key]) !== false);
+ }
+
+
+ /** isGranted
+ *
+ * checks if user is granded some role(s)
+ * from the array of roles that are passed
+ *
+ * ex. $token->isGranted(['editor', 'designer']) ...
+ * returns true if the user is editor or designer (or both)
+ *
+ */
+ public function isGranted(array $roles): bool
+ {
+ // if (!is_null($userToken = $this->getUserToken())) {
+ if (is_null($userToken = $this->getUserToken())) {
+ return false;
+ }
+
+ if ($userToken->getUser() instanceof UserInterface) {
+ return (!empty(array_intersect($roles, $userToken->getUser()->getRoles())));
+ }
+
+ return false;
+ }
+
+
+ /** createUserToken()
+ * == serializes user and stores it into session
+ * so $_SESSION[DEFAULT_PREFIX_KEY] has the serialized representaion of user
+ */
+ public function createUserToken(UserInterface $user): UserTokenInterface
+ {
+ $userToken = new UserToken($user);
+ $_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY] = $userToken->serialize();
+
+ return $userToken;
+ }
+
+
+ /** logout
+ * == clear session
+ *
+ */
+ public function logout(): void
+ {
+ if ($this->hasUserToken()) {
+ unset($_SESSION[UserTokenInterface::DEFAULT_PREFIX_KEY]);
+ }
+ }
+
+}
diff --git a/core/classes/authentication/Core/UserManagerInterface.php b/core/classes/authentication/Core/UserManagerInterface.php
new file mode 100644
index 0000000..05110fb
--- /dev/null
+++ b/core/classes/authentication/Core/UserManagerInterface.php
@@ -0,0 +1,25 @@
+<?php
+/** Interface UserManagerInterface
+ *
+ * @package DevCoder\Authentication\Core
+ *
+ */
+# 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/core/classes/authentication/Token/UserToken.php b/core/classes/authentication/Token/UserToken.php
new file mode 100644
index 0000000..b9bb024
--- /dev/null
+++ b/core/classes/authentication/Token/UserToken.php
@@ -0,0 +1,44 @@
+<?php
+/** Class UserToken
+ *
+ * @package DevCoder\Authentication
+ *
+ */
+# namespace DevCoder\Authentication\Token;
+#
+# use DevCoder\Authentication\UserInterface;
+
+class UserToken implements UserTokenInterface
+{
+ /**
+ * @var UserInterface
+ */
+ private $user;
+
+
+ public function __construct(UserInterface $user)
+ {
+ $this->user = $user;
+ }
+
+
+ /** getUser
+ *
+ */
+ public function getUser(): UserInterface
+ {
+ return $this->user;
+ }
+
+
+ /** serialize()
+ *
+ * serializes the user structure (with user's property values)
+ * ... then it will be saved into session[DEFAULT_PREFIX_KEY]
+ *
+ */
+ public function serialize(): string
+ {
+ return serialize($this);
+ }
+}
diff --git a/core/classes/authentication/Token/UserTokenInterface.php b/core/classes/authentication/Token/UserTokenInterface.php
new file mode 100644
index 0000000..8e8b642
--- /dev/null
+++ b/core/classes/authentication/Token/UserTokenInterface.php
@@ -0,0 +1,18 @@
+<?php
+/** Interface UserTokenInterface
+ *
+ * @package DevCoder\Authentication\Token
+ *
+ */
+# namespace DevCoder\Authentication\Token;
+#
+# use DevCoder\Authentication\UserInterface;
+
+interface UserTokenInterface
+{
+ const DEFAULT_PREFIX_KEY = 'user_security';
+
+ public function getUser(): UserInterface;
+
+ public function serialize(): string;
+}
diff --git a/core/classes/authentication/User.php b/core/classes/authentication/User.php
new file mode 100644
index 0000000..babc7fd
--- /dev/null
+++ b/core/classes/authentication/User.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Class User
+ *
+ * based on
+ * @package DevCoder\Authentication
+ *
+ */
+# namespace DevCoder\Authentication;
+
+class User implements UserInterface
+{
+
+ // @var string
+ private $userName;
+
+ // @var string
+ private $password;
+
+ // @var array
+ private $roles = [];
+
+ // @var array
+ private $privileges = [];
+
+ // @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;
+ }
+
+
+ /** setEnabled
+ *
+ * @param bool $enabled
+ * @return User
+ */
+ public function setEnabled(bool $enabled): self
+ {
+ $this->enabled = $enabled;
+ return $this;
+ }
+}
diff --git a/core/classes/authentication/UserInterface.php b/core/classes/authentication/UserInterface.php
new file mode 100644
index 0000000..b035215
--- /dev/null
+++ b/core/classes/authentication/UserInterface.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Interface UserInterface
+ *
+ * @package DevCoder\Authentication
+ *
+ */
+# namespace 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/core/classes/authentication/info.md b/core/classes/authentication/info.md
new file mode 100644
index 0000000..e6d1372
--- /dev/null
+++ b/core/classes/authentication/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/core/classes/cache/Cache_interface.php b/core/classes/cache/Cache_interface.php
new file mode 100644
index 0000000..7c49789
--- /dev/null
+++ b/core/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/core/classes/cache/FileCache.php b/core/classes/cache/FileCache.php
new file mode 100644
index 0000000..fb7eee5
--- /dev/null
+++ b/core/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/core/classes/cache/MemcachedCache.php b/core/classes/cache/MemcachedCache.php
new file mode 100644
index 0000000..c89d86a
--- /dev/null
+++ b/core/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/core/classes/cache/RedisCache.php b/core/classes/cache/RedisCache.php
new file mode 100644
index 0000000..ec50f08
--- /dev/null
+++ b/core/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/core/classes/session/DatabaseSession.php b/core/classes/session/DatabaseSession.php
new file mode 100644
index 0000000..aa31b62
--- /dev/null
+++ b/core/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/core/classes/session/DefaultSession.php b/core/classes/session/DefaultSession.php
new file mode 100644
index 0000000..079ab9d
--- /dev/null
+++ b/core/classes/session/DefaultSession.php
@@ -0,0 +1,42 @@
+<?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
+ // (the default way)
+ // and let the session* functions
+ // do what they know
+ 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/core/classes/session/FilesSession.php b/core/classes/session/FilesSession.php
new file mode 100644
index 0000000..a3c8437
--- /dev/null
+++ b/core/classes/session/FilesSession.php
@@ -0,0 +1,138 @@
+<?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->sessionName = SESSION_NAME;
+ $this->sess_filename = FILESESSION_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($this->sessionName);
+ 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;
+ $data = file_get_contents($filename);
+
+ return @unserialize($data);
+ }
+
+ /** write
+ * data serielized already by php's internal session engine
+ */
+ public function write($sessionId, $data): bool
+ {
+ $h = fopen($filename, 'w');
+ if (fwrite($h, serialize($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/core/classes/session/SessionHandlerInterface.php b/core/classes/session/SessionHandlerInterface.php
new file mode 100644
index 0000000..cf6efac
--- /dev/null
+++ b/core/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()
+ *
+ */
diff --git a/core/cli/cli-cache.php b/core/cli/cli-cache.php
new file mode 100644
index 0000000..57d8090
--- /dev/null
+++ b/core/cli/cli-cache.php
@@ -0,0 +1,65 @@
+#!/usr/local/bin/php
+<?php
+
+/** cli-cache
+ * regenerates/refreshes caches.
+ * ------------------------------------------------------
+ *
+ * The sctipt implements a micro-anom application
+ * that queries directry the project's models.
+ *
+ * Advantages vs curl-cache:
+ * ---
+ * + Runs out of the web/apache interface;
+ * + Has less requirements, uses less total resources;
+ * + Avoids the typical block/waiting/receiving delays;
+ * + (theoretically) it is a faster implementation
+ *
+ * ------------------------------------------------------
+ */
+$t0 = microtime(true); // time benchmark
+
+require_once 'micro/term_utilities.php'; // terminal colors
+
+echo textColor("\nInterface: ", NORMAL) . textColor(php_sapi_name(), GREEN). "\n";
+
+
+/** anom-Cli app initialization
+ * ------------------------------------------------------
+ */
+define('MINI_APP_BASE', __DIR__.'/');
+echo "Base directory is ". textColor(MINI_APP_BASE, GREEN) ."\n";
+
+require_once MINI_APP_BASE.'/micro/setup.php';
+echo "System is ready;\n";
+
+
+
+try {
+ # your code goes here ------------------------------------------------------
+
+
+ // Re-create category_products tree Cache
+ app\controllers\cli\CliContoller::cacheCategoriesTree();
+
+ $categories = app\controllers\cli\CliContoller::getCategories();
+
+
+ foreach($categories as $key => $category) {
+
+ echo textColor( textWidth($category['Title'], 72), NORMAL);
+ app\controllers\cli\CliContoller::cacheCategoryByUrl($category['FullFriendlyUrl']);
+ }
+
+ # end of script ------------------------------------------------------------
+
+} catch(Exception $e) { // Basic error handling
+ echo 'Caught exception: ', textColor($e->getMessage(), ERROR), "\n";
+ die();
+}
+
+
+$dt = number_format( microtime(true) - $t0 , 0) ."sec";
+
+// echo ending...
+echo textColor("\nOperation Completed {$dt}\n\n", NORMAL);
diff --git a/core/cli/curl-cache.php b/core/cli/curl-cache.php
new file mode 100755
index 0000000..6ab2e93
--- /dev/null
+++ b/core/cli/curl-cache.php
@@ -0,0 +1,155 @@
+#!/usr/local/bin/php
+<?php
+
+/** curl-cache
+ * regenerates/refreshes caches
+ * ------------------------------------------------------
+ *
+ * This scipt makes requests to the project's web API,
+ * These API calls SHOULD be impemented to cache some
+ * resource-expensive queries, so in result the script
+ * shall manage to create cache.
+ *
+ * NOTE:
+ * The script does not implement the anom framerork
+ * and it is provided for documentation puproses.
+ *
+ * ------------------------------------------------------
+ */
+
+// some defines and an into message
+// -----------------------------------------------------------------------------
+
+require_once 'micro/term_utilities.php'; // terminal utilites
+
+define('API_ROOT','http://127.0.0.1:8080/api/');
+
+define('BATCH_LENGTH', 3); // see notes later on the script
+
+
+
+echo "\033[32mInterface: ". php_sapi_name() ."\033[39m\n\n";
+
+$t0 = microtime(true);
+
+/** supplumentary functions (curl)
+ * -----------------------------------------------------------------------------
+ */
+
+/** single Curl get call
+ *
+ * @param $call (string): url that needs to be called
+ *
+ */
+function singleCurl($call) {
+ $cSRC = curl_init(); // create curl resource for SouRCe end-point
+ curl_setopt($cSRC, CURLOPT_URL, API_ROOT.$call); // set url
+ curl_setopt($cSRC, CURLOPT_RETURNTRANSFER, 1); //return the transfer as a string
+ $output = curl_exec($cSRC); // $output contains the output string in json format
+ curl_close($cSRC); // close curl resource to free up system resources
+
+ $json = json_decode( $output ); // decode json to php array
+
+ if (!$json->success) {
+ echo "\033[91mError reading " . $call ."\n\033[39m";
+ return false;
+ }
+
+ return $json;
+}
+
+/** multiple curl get calls
+ *
+ * @param $array: an array of urls to be called
+ *
+ */
+function multiCurl($array) {
+
+ $multiCurl = array(); // array of curl handlers
+ $result = array(); // data to be returned
+ $mh = curl_multi_init(); // multi handler
+ $module = array(); // array of module names / 1:1 to $result array
+
+ foreach ($array as $i => $category) {
+ $module[$i] = $category->ID; // guid (module name)
+ $fetchURL = API_ROOT .'url/'. $category->FullFriendlyUrl;
+ $multiCurl[$i] = curl_init();
+ curl_setopt($multiCurl[$i], CURLOPT_URL,$fetchURL);
+ curl_setopt($multiCurl[$i], CURLOPT_HEADER,0);
+ curl_setopt($multiCurl[$i], CURLOPT_RETURNTRANSFER,1);
+ curl_multi_add_handle($mh, $multiCurl[$i]);
+ }
+
+ $index=null;
+
+ do {
+ curl_multi_exec($mh,$index);
+ } while($index > 0);
+
+ foreach($multiCurl as $k => $ch) {
+ $result[$k] = curl_multi_getcontent($ch);
+ curl_multi_remove_handle($mh, $ch);
+ }
+ curl_multi_close($mh); // close multi-curl
+}
+
+// force printing string with specified length
+function forceLen($str, $len = 72) {
+ $space = " ";
+ for($i = 0 ; $i<$len ; $i++) $space .= ".";
+ return mb_substr($str.$space, 0, $len-1) ." ";
+}
+
+
+/** main procedure
+ * -----------------------------------------------------------------------------
+ */
+
+echo "\033[39mReading Tree of product categories...\n";
+$tree = singleCurl('tree'); // ask for categories tree
+
+if ($tree !==false) echo "\033[32mCategories tree is cached\033[39m\n\n";
+
+$categories = singleCurl('category');
+
+// request every category to create cache if not exist
+// send requests in small batches to avoid enormous server-stress
+echo "\033[39mRequesting re-Cache for every category\033[39m\n";
+echo "\033[33mPlease Wait...\n\n";
+
+$buffer = []; // buffer array
+$counter = 0; // counter to track items in buffer
+foreach($categories->result as $category) {
+
+ echo "\033[39m". forceLen($category->Title) ."\033[33m"."Sent.\n\033[39m";
+
+ $counter++;
+
+ // try several batch lengths to establish the ideal one that produces
+ // results in a smooth quite fase rthm, without stressing the server;
+ // so that visitors can navigate the production site white the script
+ // is running;
+ // for my testing this number is 3-5. I set the cou 3 items give fast results without stressing the server
+ // so "if ($counter > 2) { ... " is the smoothest option
+ if ($counter > 2) {
+ multiCurl($buffer);
+ $counter = 0;
+ $buffer = [];
+ }
+
+ $buffer[] = $category;
+
+}
+
+multiCurl($buffer); // run once more for the last non-filled buffer
+
+$dt = number_format( microtime(true) - $t0 , 1) ."sec";
+
+// echo ending...
+echo "\n\033[32mOperation Completed \033[39m({$dt})\n\n";
+echo "\033[39m"; // back to default color;
+
+
+// check
+// http://www.idein.it/joomla/14-docker-php-apache-with-crontab
+?> \ No newline at end of file
diff --git a/core/cli/info.md b/core/cli/info.md
new file mode 100644
index 0000000..39f8c11
--- /dev/null
+++ b/core/cli/info.md
@@ -0,0 +1,23 @@
+# cli
+
+This section is used to setup an anom micro-framework for the cli-interface.
+The cli interface is commonly used to support and optimize the whole app.
+
+A cli script may run manualy, for example using...
+
+ php cli-cache.php
+
+although...
+
+it is recommended to run periodically using the cron service:
+
+ 15 * * * * php path/to/cli-cache.php
+
+or ...
+
+when a docker instanse of the project fires up; for example including
+the following line of code inside the docker/bin/docker-entrypoint.sh
+(runs the script after 5 minutes delay to make sure that the instance
+is up and running)
+
+ at -f /var/www/core/cli/refresh-cache.php -t now +5 minutes
diff --git a/core/cli/micro/setup.php b/core/cli/micro/setup.php
new file mode 100644
index 0000000..ca2fd40
--- /dev/null
+++ b/core/cli/micro/setup.php
@@ -0,0 +1,120 @@
+<?php
+/** Micro anom framework
+ * minimum setup with the rich features of the framework;
+ *
+ * in a symbolic way: { Micro_anom summarizes anom }
+ * -----------------------------------------------------------------------------
+ *
+ * (+) Use it mainly in cli-interface scripts to
+ * access your application's data intarnaly
+ *
+ * (+) Implement routine tasks and schedule the
+ * execution using cron deamon
+ *
+ * (+) Avoid public/web-based API calls
+ *
+ * (+) Create batch proccessing scripts re-using
+ * your project's implemented methods;
+ *
+ * Don't haves
+ * ---
+ * In your cli-interfaced micro-framework you most
+ * probably won't need some of the anom's central
+ * objects (Request, Router, Session, Contollers).
+ *
+ * Controller is the script, Router is the scheduler,
+ * Session and Authentication is unnecessary (just
+ * put an...
+ * ```if (php_sapi_name() != 'cli') return false;```
+ * and no call will access the code but the cli.
+ *
+ *
+ * What you do have?
+ * ---
+ * + Database and Cache objects,
+ * + all the implemented methods,
+ * + Proxy, Repositories etc...
+ *
+ * and these are more than enough to write easily
+ * a fast, secure, powerful script that handles
+ * your data in every way you need.
+ */
+
+echo textColor("Setup environment....", GREEN) ."\n";
+
+// get constants
+// -----------------------------------------------------------------------------
+require_once realpath(MINI_APP_BASE.'../config/constants.php');
+
+// anom typical defines
+// -----------------------------------------------------------------------------
+define('PRODUCTION', false);
+
+define('APP_NAME', 'e-Classroom'); // Application Name
+
+define('APP_VERSION', 'v0.1'); // Application version
+
+define('APP_ROOT', realpath(MINI_APP_BASE.'../../public'));
+
+// Cache driver and caching TTLs
+// -----------------------------------------------------------------------------
+define('CACHE_DRIVER', 'FileCache');
+
+// Root objects like product-categories tree
+define('CACHE_ROOT_TTL', 28800); // 8 hours
+
+// Product Category
+define('CACHE_CATEGORY_TTL', 18000); // 5 hours
+
+// Product
+define('CACHE_PRODUCT_TTL', 3600); // 1 hour
+
+define('FILECACHE_PATH', realpath(MINI_APP_BASE.'../../storage/cache/').'/');
+
+echo "Cache Directory is ". textColor(FILECACHE_PATH, GREEN)."\n";
+
+
+// database
+// -----------------------------------------------------------------------------
+
+$ini_array = parse_ini_file(realpath(MINI_APP_BASE."../auth/.env"));
+define('DB_NAME', $ini_array['DB_NAME']);
+define('DB_USER', $ini_array['DB_USER']);
+define('DB_PASS', $ini_array['DB_PASS']);
+define('PDO_HOST', $ini_array['PDO_HOST']);
+echo "Application database is ". textColor(DB_NAME, GREEN) ."\n";
+
+define('DB_TIMEZONE', "SET time_zone = 'Europe/Athens'");
+
+
+// load autoloader
+// -----------------------------------------------------------------------------
+require_once realpath(MINI_APP_BASE.'../../vendor/autoload.php');
+
+
+
+// Attache database to the Registry a vow
+Registry::vow('database', function() { return new Database(); });
+
+
+// Attach Cache to the Resitry as a vow
+Registry::vow('cache', function() { return new (CACHE_DRIVER)(); });
+
+
+require_once realpath(MINI_APP_BASE.'../helpers/design_patterns.php');
+
+
+# Registry::set('reg', 'Registry is working');
+# echo Registry::get('reg'), "\n\n";
+
+
+# NOTE:
+# php does not provide error and exception handling
+# for the cli interface; you still can handle the
+# exceptions using standard try {...} catch { }
+#
+# try {
+# // code
+# } catch (Exception $exc) {
+# echo 'Caught exception: ', $exc->getMessage(), "\n"
+# } \ No newline at end of file
diff --git a/core/cli/micro/term_utilities.php b/core/cli/micro/term_utilities.php
new file mode 100644
index 0000000..971c36e
--- /dev/null
+++ b/core/cli/micro/term_utilities.php
@@ -0,0 +1,44 @@
+<?php
+
+define('NORMAL', "\033[39m"); // white
+define('SUCCESS', "\033[32m"); // green
+define('FAIL', "\033[1;31m"); // red
+define('ERROR', "\033[1;31m"); // red
+define('PROGRESS', "\033[33m"); // orenge
+
+define('WHITE', "\033[39m"); // white
+define('GREEN', "\033[32m"); // green
+define('RED', "\033[1;31m"); // red
+define('ORANGE', "\033[33m"); // orenge
+
+
+function textColor($text, $mode = NORMAL) {
+ switch ($mode) {
+ case SUCCESS:
+ case GREEN:
+ return SUCCESS.$text.NORMAL;
+ break;
+
+ case FAIL:
+ case RED:
+ return FAIL.$text.NORMAL;
+ break;
+
+ case PROGRESS:
+ case ORANGE:
+ return PROGRESS.$text.NORMAL;
+ break;
+
+ default:
+ return NORMAL.$text;
+ }
+}
+
+
+// force printing string with specified length
+function textWidth($str, $len = 72) {
+ $space = " ";
+ for($i = 0 ; $i<$len ; $i++) $space .= ".";
+
+ return mb_substr($str.$space, 0, $len-1) ." ";
+} \ No newline at end of file
diff --git a/core/config/anom_settings.php b/core/config/anom_settings.php
new file mode 100644
index 0000000..50056bf
--- /dev/null
+++ b/core/config/anom_settings.php
@@ -0,0 +1,325 @@
+<?php
+
+/** CONFIGURATION PATAMETRES
+ * -----------------------------------------------------------------------------
+ * This is the first file you need to setup
+ * in order to start a new application
+ *
+ * NOTE:
+ * You may rewrite the defines manualy setting any values manualy
+ * Here we propose that you use an .env file (with is a very safe option)
+ * the env file is saved into `/core/auth` folder
+ */
+
+if (file_exists("../core/auth/.env")) {
+ $ini = parse_ini_file("../core/auth/.env");
+}
+
+// DEFINE WHETTHER THE APP RUNS ON PRODUCTION
+// Don't forget to change this setting
+// when deploying to a different stage
+// -----------------------------------------------------------------------------
+
+# // define by OS parameter
+# if (getenv('ENVIRONMENT')) {
+# if (getenv('ENVIRONMENT') == 'PRODUCTION') {
+# define('PRODUCTION', true);
+# } else {
+# define('PRODUCTION', false);
+# }
+# }
+
+# define by .env file
+define('PRODUCTION', ($ini['ENVIRONMENT'] == 'production') );
+
+
+
+// Custom Names ////////////////////////////////////////////////////////////////
+// -----------------------------------------------------------------------------
+
+define('APP_NAME', 'e-Classroom'); // Application Name
+
+define('APP_VERSION', 'v0.2'); // Application version
+
+define('SESSION_NAME', 'clroom'); // Session Name
+
+
+
+/** APPLICATION PATHS ///////////////////////////////////////////////////////////
+ * -----------------------------------------------------------------------------
+ *
+ * Obligarory
+ * These are needed in order to run the bare minimum MVC system
+ * They also make code easier to read
+ *
+ * NOTE:
+ * probably you don't need to chage these defines
+ * -----------------------------------------------------------------------------
+ */
+
+define('APP_ROOT', dirname(get_included_files()[0]) );
+
+define('URL_ROOT', '/');
+
+define('CREDENTIALS', '../core/config/credentials.php');
+
+define('INIT_APPLICATION', '../core/config/init.php');
+
+define('RENDERING_SYSTEM', '../core/helpers/render.php');
+
+define('VIEWS_DIRECTORY', APP_ROOT.'/app/views/');
+
+define('TESTS_DIRECTORY', APP_ROOT."/../tests/");
+
+
+/** AUTOLOADER /////////////////////////////////////////////////////////////////
+ * -----------------------------------------------------------------------------
+ * An autoloader for classes is required;
+ *
+ *
+ * OPTION 1: COMPOSER
+ * Composer is recommended and makes much more than simple autoloding
+ */
+define('AUTOLOADER' , '../vendor/autoload.php');
+
+/** ...
+ *
+ * OPTION 2: Custom Autoloader
+ * If Composer is not supported, another autoloading proccess required
+ * Comment/Disable the 1st option and uncomment/Enable the next line
+ */
+# define('AUTOLOADER' , '../core/helpers/autoload.php');
+
+/** CLASSPATHS : array of paths where classes are saved
+ * Needed only when the custom autoloader is used
+ * (like autoload.classmap section of Composer)
+ */
+# define('CLASSPATHS', array(
+# '/../core/classes/',
+# '/app/controllers/',
+# '/app/models/',
+# '/../vendor/'
+# )
+# );
+
+
+
+ /** SESSION HANDLING //////////////////////////////////////////////////////////
+ * ----------------------------------------------------------------------------
+ *
+ * there are several session drivers for session handling:
+ *
+ * 'DefaultSession' : uses the default (file-based) php session management
+ * 'FileSession' : defines a custom file-based session mechanism
+ * 'DatabaseSession' : mechanism that stores Session data into a DataBase
+ * ----------------------------------------------------------------------------
+ */
+
+define('SESSION_DRIVER', 'DefaultSession');
+
+// if FileSession is selected, a path for session-files is needed
+# define('FILESESSION_PATH', '../storage/session');
+
+/* ADVANCED OPTIMIZATION ///////////////////////////////////////////////////////
+ * -----------------------------------------------------------------------------
+ *
+ * APP_CACHE defines the cache-driver;
+ * accepted values are the name of the Cache-interface impementations (the exact
+ * names of the classes)
+ *
+ * 'FileCache' : caching in filesystem; fair if caching on HDD; great on SSD
+ * 'RedisCache' : caching in Redis server; generally recommended if available
+ * 'MemCachedCache' : caching in Memcached server; recommended if available
+ *
+ * NOTE: FileCache on a NVMe SDD is the fastest option;
+ * Redis and Memcached are really-fast caching options and are available for
+ * scaling horizontaly your application (eache one has it's own strngths;
+ * study, then choose the one that fulfills your needs;
+ * -----------------------------------------------------------------------------
+ */
+
+define('CACHE_DRIVER', 'FileCache');
+
+
+/** Caching expiration times fpr various expensive objects
+ * -----------------------------------------------------------------------------
+ */
+
+// Root objects like product-categories tree
+define('CACHE_ROOT_TTL', 28800); // 8 hours
+
+// Product Category
+define('CACHE_CATEGORY_TTL', 18000); // 5 hours
+
+// Product
+define('CACHE_PRODUCT_TTL', 3600); // 1 hour
+
+
+
+
+// CONNECTIONS ...
+
+define('FILECACHE_PATH', APP_ROOT.'/../storage/cache/'); // if CACHE_DRIVER is set to 'FileCache'
+
+define('MEDIA_STORAGE_ROOT', APP_ROOT.'/../storage/uploads/'); // Media files root directory
+
+# define('REDIS_HOST', '127.0.0.1'); // if CACHE_DRIVER is set to 'RedisCache'
+
+# define('MEMCAHCED_SERVER', '127.0.0.1'); // if CACHE_DRIVER is set to 'MemCached'
+
+
+
+/** SECURITY SETTINGS ///////////////////////////////////////////////////////////
+ * -----------------------------------------------------------------------------
+ *
+ * Cross Site Request Forgery
+ * ---
+ * Enables a CSRF cookie token to be set. When set to TRUE, token will be
+ * checked on a submitted form. If you are accepting user data, it is strongly
+ * recommended CSRF protection be enabled.
+ * -----------------------------------------------------------------------------
+ */
+
+define('CSRF_PROTCTION', false);
+
+define('CSRF_TOKEN_NAME', 'csrf_test_name'); // token name
+
+define('CSRF_COOKIE_NAME', 'csrf_cookie_name'); // cookie name
+
+define('CSRF_EXPIRE', 7200); // The number in seconds the token should expire.
+
+define('CSRF_REGENERATE', TRUE); // Regenerate token on every submission
+
+define('CSRF_EXCLUDE_URIS', array()); // Array of URIs which ignore CSRF checks
+
+
+
+
+/** CREDENTIALS
+ * -----------------------------------------------------------------------------
+ */
+
+/** SERVICE PARAMETRES
+ * -----------------------------------------------------------------------------
+ *
+ * Connection parametres and credentials for accssing services
+ * needed by the application; Such services may be..
+ *
+ * - RDBMS
+ * -- MySQL
+ * -- PotgresSQL
+ *
+ * - Caching services
+ * -- Redis
+ * -- MemCached
+ *
+ * Edit only the constants needed by the application;
+ * Comment those you do not need;
+ *
+ * /////////////////////////////////////////////////////////////////////////////
+ */
+
+/** DATABASE CONNECTION PARAMETRES
+ * -----------------------------------------------------------------------------
+ * Production and staging environments may use different databases.
+ *
+ * A safe practice is to keep credentials outside plain files like this one
+ * in environmental variable or other secret file etc.
+ *
+ * Uncomment/enable each group of definitions suits your case to define
+ * the credentials needed for accessing the database
+ */
+
+/** This is a SAFE method (define credentials as OS environmental variables)
+ * -----------------------------------------------------------------------------
+ */
+# define('DB_NAME', getenv('DB_NAME')); // database name
+#
+# define('DB_USER', getenv('DB_USER')); // database user-name
+#
+# define('DB_PASS', getenv('DB_PASS')); // user's pass
+#
+# define('PDO_HOST', getenv('PDO_HOST')); // database host (connection string)
+
+
+/** This is another SAFE method (keep credentials in an .env file)
+ * -----------------------------------------------------------------------------
+ */
+
+define('DB_NAME', $ini['DB_NAME']);
+
+define('DB_USER', $ini['DB_USER']);
+
+define('DB_PASS', $ini['DB_PASS']);
+
+define('PDO_HOST', $ini['PDO_HOST']);
+
+
+ /** This is just good enough
+ * -----------------------------------------------------------------------------
+ */
+
+ # if (!PRODUCTION) { // == Deployed on staging
+ #
+ # // STAGING SETTINGS:
+ #
+ # define('DB_NAME', 'dev_db_name');
+ #
+ # define('DB_USER', 'devUserName');
+ #
+ # define('DB_PASS', getenv('DBPASSWORD'));
+ #
+ # // PDO_HOST can be a hostname/port combination -or- a unix socket
+ # // ...examples:
+ # // define('PDO_HOST', 'host=/localhost') ## hostname case
+ # // define('PDO_HOST', 'host=/localhost;port=3456') ## hostname/port case
+ # // define('PDO_HOST', 'unix_socket=/sql/ex123:europe:some-db'); ## unix socket
+ # define('PDO_HOST', 'unix_socket=/cloudsql/name-123456:europe-west4:name-db-eu');
+ #
+ # } else { // == Deployed on production
+ #
+ # // PRODUCTION SETTINGS:
+ #
+ # define('DB_NAME', 'your_db_name');
+ # define('DB_USER', 'dbusername');
+ # define('DB_PASS', getenv('DBPASSWORD'));
+ # define('PDO_HOST', 'unix_socket=/cloudsql/name-123456:europe-west4:name-db-eu');
+ #
+ # }
+
+define('DB_TIMEZONE', "SET time_zone = 'Europe/Athens'");
+
+ /** NOTE:
+ * Both Redis and Memcached configurations are given as a template to work on;
+ * In most cases the default values should do the job -- of course you need to
+ * read the README file (check the project's root);
+ * As these caching services are not fully tested you may need to ochestrate
+ * the services in detail or edit the connection strings into the core classes
+ * (hosted under the '/core/classes/cacher' folder)
+ */
+
+
+ /** REDIS SERVICE
+ * -----------------------------------------------------------------------------
+ * Most of the times Redis is running on 'localhost' (host = '127.0.0.1')
+ * When implemented via anom's docker-composer (redis via bridge) then you need
+ * do declare the hostname as 'redis'
+ */
+ #
+ # if (!defined('REDIS_HOST')) define('REDIS_HOST', 'redis');
+ #
+ # if (!defined('REDIS_PORT')) define('REDIS_PORT', 6379);
+ #
+ # if (!defined('REDIS_PASS')) define('REDIS_PASS', null);
+
+
+ /** MEMCACHED
+ * -----------------------------------------------------------------------------
+ * Memcached looks very much like Redis (plus, both are serving from RAM)
+ * Usualy Memcashed is running localy so host is 'localhost' ('127.0.0.1')
+ * If you use the framework's docker-composer implementation then
+ * the hostname is 'anomemcached'
+ */
+ #
+ #if (!defined('MEMCACHE_HOST')) define('MEMCACHE_HOST', 'anomemcached');
+ \ No newline at end of file
diff --git a/core/config/constants.php b/core/config/constants.php
new file mode 100644
index 0000000..d2ecc41
--- /dev/null
+++ b/core/config/constants.php
@@ -0,0 +1,28 @@
+<?php
+/** Constants
+ * ---
+ */
+
+// COMMON CONTENT TYPES
+// -----------------------------------------------------------------------------
+define('COMMON_CONTENT_TYPES', array(
+ 'text' => 'text/html; charset=UTF-8',
+ 'html' => 'text/html; charset=UTF-8',
+ 'json' => 'application/json; charset=utf-8',
+ 'js' => 'application/javascript; charset=utf-8',
+ 'css' => 'text/css',
+ 'png' => 'image/png',
+ 'jpeg' => 'image/jpeg',
+ 'webp' => 'image/web'
+ )
+);
+
+
+// PROXY_FLAGS
+// -----------------------------------------------------------------------------
+// These options represend altered/non-default behaviour
+// and will be resolved bitwise, so use powers of 2
+// -----------------------------------------------------------------------------
+define('PROXY_CACHE_ERRORS', 1); // cache result even if error
+define('PROXY_IGNORE_CACHE', 2); // ignore cache if exist
+define('PROXY_DO_NOT_CACHE', 4); // do not cache result
diff --git a/core/config/init.php b/core/config/init.php
new file mode 100644
index 0000000..589a486
--- /dev/null
+++ b/core/config/init.php
@@ -0,0 +1,64 @@
+<?php
+
+/** initialize System
+ * ---
+ * Finalalize the MVC framework and construct the central objects
+ * (Registry and Request)
+ *
+ * Include _anything_ that is common practice on the request life-cycle
+ * of your project and does not need to be shown in the /public/index.php
+ * Usualy here you shall initialize supplamentary services like Sessions
+ * / Database / Caching etc.
+ *
+ * NOTE:
+ * Always try to keep the app as light as possible, so load only what is
+ * critical for the MVC to operate;
+ * anything else can be injected on-demand
+ */
+
+// setup error-handliing
+// -----------------------------------------------------------------------------
+require_once '../core/helpers/error_handling.php';
+
+
+// depricated:
+/// // Load rendering sub-system
+/// // -----------------------------------------------------------------------------
+/// require_once RENDERING_SYSTEM;
+
+
+// Setup Application Engiine
+// -----------------------------------------------------------------------------
+// Setup supplementary services (derectry or as promises/vows)
+// Session, Database, Caching
+
+
+// Attache database to the Regostry a vow
+Registry::vow('database', function() { return new Database(); });
+
+
+// Attach Cache to the Resitry as a vow
+// (CACHE_DRIVER) acts as driver-wrapper
+// --- -- -- - - -
+Registry::vow('cache', function() { return new (CACHE_DRIVER)(); });
+// CHECK: if strict mode has any benefits:
+// Registry::vow('cache', function():Cache_interface { return new (CACHE_DRIVER)(); });
+
+
+// Initialize THE REQUEST
+// -----------------------------------------------------------------------------
+Registry::set('REQUEST', new Request());
+
+
+
+// load design-patterns
+// (these patterns use some of the project's central objects (Registry,
+// Cache, Database), so laod them after.
+// -----------------------------------------------------------------------------
+require_once '../core/helpers/design_patterns.php';
+
+
+// Add Routes
+// -----------------------------------------------------------------------------
+
+require_once 'app/config/init_routes.php'; // initialize routes
diff --git a/core/helpers/autoload.php b/core/helpers/autoload.php
new file mode 100644
index 0000000..c4359c6
--- /dev/null
+++ b/core/helpers/autoload.php
@@ -0,0 +1,43 @@
+<?php
+/** register class-autoloader
+ *
+ * NOTE:
+ * Composer is strongly recommended; it is a much better option
+ * and takes care for many more than just autoloading classes;
+ *
+ * This's a fair autoloader in case composer is not available;
+ * supports single and namespaced classes.
+ */
+spl_autoload_register(
+ function($className) {
+
+ // in order to support common namespaced classes
+ // replace '\' with '/' to get the path
+ $classPath = str_replace('\\', DIRECTORY_SEPARATOR, $className);
+
+ // check all possible classPaths for class
+ foreach(CLASSPATHS as $key => $basePath) {
+
+ $file = APP_ROOT . $basePath . $classPath .'.php';
+
+ // if class-file exist, include it
+ if (file_exists($file)) {
+ require_once $file;
+ break;
+ }
+
+ }
+
+ // // Feel free to extend the fanctionality
+ // // example:
+ // // custom (exception) namespaced classes loader
+ // $customNSclasses = array(
+ // '\\George\\Class' => 'path/to/George/Class',
+ // '\\Mary\\Class' => 'path/to/Marias/Class',
+ // ...
+ // );
+ // if array_key_exists($className) {
+ // require_once $customNSclasses[$className];
+ // }
+ }
+);
diff --git a/core/helpers/design_patterns.php b/core/helpers/design_patterns.php
new file mode 100644
index 0000000..d66ffa9
--- /dev/null
+++ b/core/helpers/design_patterns.php
@@ -0,0 +1,104 @@
+<?php
+/** proxy
+ * implements the proxy design pattern
+ *
+ * uses: Registry::cache
+ *
+ * algorithm:
+ * - 1: proxy gets (Class::method, Arguments) and calculates a unique key
+ * - 2: if a valid Cache for the triplete's key exists, it is served immediately
+ * - 3: otherwise calls Class::Method(Arguments), caches then serves response
+ *
+ * @param $class_method (array): a 2 (string) items array in the form
+ * : [ 'fully\qualifies\ClassName' , 'methodName' ]
+ * : Using the ClassName::class notation is strongly recommended.
+ * @param $args: array of arguments (passed to the Class::Method())
+ * @param $ttl: time (in sec) where the data will remain valid (not expired)
+ * @param $options: bitwise flags*
+ * ---
+ * @return $data (mixed)
+ *
+ * (*) option flags
+ * all option flags represend non default behaviour of proxy
+ * ---
+ * PROXY_CACHE_ERRORS : cache value even if callback returns error
+ * PROXY_IGNORE_CACHE : ignore if cache exist; get result from callback
+ * PROXY_DO_NOT_CACHE : do not cache the result (even if from callback)
+ */
+function proxy(array $callable, array $args, int $ttl, int $options=0)
+{
+ // resolve option flags
+ $cache_errors = ($options&PROXY_CACHE_ERRORS) ? true : false;
+ $ignore_cache = ($options&PROXY_IGNORE_CACHE) ? true : false;
+ $do_not_cache = ($options&PROXY_DO_NOT_CACHE) ? true : false;
+
+ // we're gonna use cache;
+ $cache = Registry::use('cache');
+
+
+ // check validity of callable argument
+ // -------------------------------------------------------------------------
+ if ((!is_callable($callable)) || (!is_array($callable))) {
+
+ throw new Exception(
+ 'Proxy\'s first parameter has to be a callable array [fully\\qualified\\ClassName, method], passed:'.print_r($callable, true)
+ );
+
+ }
+
+ // create a unique key
+ // -------------------------------------------------------------------------
+ list($class, $method) = $callable;
+ $key = sha1(
+ $class .':'. $method .':'. json_encode(
+ $args, JSON_UNESCAPED_LINE_TERMINATORS|JSON_UNESCAPED_UNICODE
+ )
+ );
+
+ // search cache if data for this key exist
+ // -------------------------------------------------------------------------
+ if (!$ignore_cache) {
+ $data = $cache->get($key);
+
+ } else { $data = false; } // ignore cache? pass false
+
+
+ if ($data !== false) { // if exists, serve cached data
+
+ // if cashed data exist
+ // ---------------------------------------------------------------------
+ # if (!PRODUCTION) Benchmark::add_spot('proxy-cached');
+ if ($data == '_ERROR_') return false; // previous cached error
+ else return $data; // previous cache ()
+
+
+ } else { // else (not exist)...
+
+ // no data on cache -> get data from Class::method( arguments )
+ // ---------------------------------------------------------------------
+ $data = call_user_func_array($callable, $args);
+
+ // handle newly created data
+ // ---------------------------------------------------------------------
+
+ // if faulty data, return false
+ if ($data == false) {
+
+ if ($cache_errors && !$do_not_cache) { // cache the error
+ $cache->set($key, '_ERROR_', $ttl);
+ }
+
+ # if (!PRODUCTION) Benchmark::add_spot('proxy-new-error');
+ return false;
+ }
+
+ // store to cache
+ if (!$do_not_cache) {
+ $cache->set($key, $data, $ttl);
+ }
+
+ # if (!PRODUCTION) Benchmark::add_spot('proxy-new');
+ return $data; // serve data
+ }
+
+}
diff --git a/core/helpers/error_handling.php b/core/helpers/error_handling.php
new file mode 100644
index 0000000..95db98c
--- /dev/null
+++ b/core/helpers/error_handling.php
@@ -0,0 +1,119 @@
+<?php
+/** ERROR HANDLING SECTION
+ * handles exceptions and errors (even fatal)
+ * -----------------------------------------------------------------------------
+ * The code...
+ * + Catches and handles all Errors and Throwable exceptions;
+ * - creates the error messages (for the requester and developer)
+ * ? TODO: routes the error messages to the logging channels
+ */
+
+
+// Global array to save errors
+$anom_ERRORS = []; // ok, Globals are not recommended, but...
+ // this one collects only errors (about the application);
+ // it is oriented for the developer; not for the request.
+
+
+/** Error handler,
+ * passes flow over the exception logger with new ErrorException.
+ */
+function handle_error(int $type, string $message, string $file, int $line)
+{
+ handle_exception( new ErrorException($message, 0, $type, $file, $line) );
+}
+
+
+/** Uncaught exception handler.
+*/
+function handle_exception(Throwable $e)
+{
+ global $anom_ERRORS;
+
+ // full message description to be logged
+ $message = get_class($e) .": ". $e->getMessage()
+ . " on file '". $e->getFile() . "' at line {$e->getLine()}";
+
+ $anom_ERRORS[] = $message; // store the error
+}
+
+
+/**
+ * Catch fatal error work-around
+ * (set_error_handler not working on fatal errors).
+ */
+function on_shutdown_check_for_fatal()
+{
+ global $anom_ERRORS;
+
+ $error = error_get_last();
+
+ if ($error != null) { // ignore 'normal' system shutdowns [exit|die]()
+
+ if ( $error["type"] == E_ERROR ) { // get as much error-info
+ handle_error(
+ $error["type"] ?? -1,
+ $error["message"] ?? 'unknown error',
+ $error["file"] ?? 'unknown file',
+ $error["line"] ?? -1
+ );
+ }
+ }
+
+ if (($anom_ERRORS != [])) {
+
+ // TODO:
+ // Implemetnig the design/layout of the error-messages
+ // output should not be an error's-handling task, thus
+ // shall be assigned to the Rendes/View level;
+ // Same about the log service channels
+
+ // TODO:
+ // FORWARD ERRORS into LOG-SYSTEM
+ // ex. into some file...
+ // file_put_contents("logs/thowable.log", $message_template .PHP_EOL, FILE_APPEND);
+ // or ...some other channer (Email, Slack, Database etc).
+ // Channes can be decided according to error severity;
+ // a file-system error loggin for backup is recommended
+ // partitioniong of errors onto file-system can be useful
+
+ // TODO:
+ // 1st: Log Errors
+ // 2nd: Show Errors
+
+ if (PRODUCTION) {
+ if (!defined('OUTPUT_STARTED')) {
+ // throw a prety-error message
+ Render::view('error/general', ['message' => 'Please forgive me, I know not what I do']);
+ }
+
+ } else {
+
+ if (!defined('OUTPUT_STARTED')) {
+ // no template loaded; render erros in general-error template
+ Render::view('error/general', ['message' => implode("<br/>And:<br/>", $anom_ERRORS)]);
+
+ } else {
+ echo '<div style="padding:1em;background:#933;width:100%;color:#fff">
+ Errors:<br />'. implode("<br/>And:<br/>", $anom_ERRORS) .'</div>';
+ }
+ }
+ }
+}
+
+
+// register callback on shutdown
+register_shutdown_function( "on_shutdown_check_for_fatal" );
+
+// register custom error handler callback
+set_error_handler( "handle_error" );
+
+// set custom exception handler function
+set_exception_handler( "handle_exception" );
+
+// send errors to stderr (instead of stdout)
+ini_set( "display_errors", "off" );
+
+// thow out all errors
+error_reporting( E_ALL );
+