From 47cbb529f5723b246125ae083a193e11481b89ef Mon Sep 17 00:00:00 2001
From: George Halkiadakis
Date: Sun, 12 Mar 2023 07:01:30 +0200
Subject: initializing classroom structure using anom framework
---
.gitignore | 72 ++
README.md | 266 +++++++
auth/.gitkeep | 0
classes/Benchmark.php | 150 ++++
classes/Cart.php | 71 ++
classes/Database.php | 331 +++++++++
classes/Registry.php | 129 ++++
classes/Repository.php | 43 ++
classes/Request.php | 94 +++
classes/Route.php | 97 +++
classes/Security.php | 923 +++++++++++++++++++++++++
classes/User.php | 111 +++
classes/authenticator/PasswordTrait.php | 40 ++
classes/authenticator/User.php | 102 +++
classes/authenticator/UserInterface.php | 15 +
classes/authenticator/UserManager.php | 69 ++
classes/authenticator/UserManagerInterface.php | 21 +
classes/authenticator/UserToken.php | 33 +
classes/authenticator/UserTokenInterface.php | 13 +
classes/authenticator/info.md | 114 +++
classes/cache/Cache_interface.php | 34 +
classes/cache/FileCache.php | 93 +++
classes/cache/MemcachedCache.php | 37 +
classes/cache/RedisCache.php | 69 ++
classes/session/DatabaseSession.php | 138 ++++
classes/session/DefaultSession.php | 35 +
classes/session/FilesSession.php | 142 ++++
classes/session/SessionHandlerInterface.php | 169 +++++
cli/micro/setup.php | 120 ++++
cli/micro/term_utilities.php | 44 ++
config/anom_settings.php | 169 +++++
config/constants.php | 28 +
config/credentials.php | 126 ++++
config/init.php | 76 ++
helpers/autoload.php | 43 ++
helpers/design_patterns.php | 104 +++
helpers/error_handling.php | 119 ++++
helpers/render.php | 338 +++++++++
38 files changed, 4578 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 auth/.gitkeep
create mode 100644 classes/Benchmark.php
create mode 100644 classes/Cart.php
create mode 100644 classes/Database.php
create mode 100644 classes/Registry.php
create mode 100644 classes/Repository.php
create mode 100644 classes/Request.php
create mode 100644 classes/Route.php
create mode 100644 classes/Security.php
create mode 100644 classes/User.php
create mode 100644 classes/authenticator/PasswordTrait.php
create mode 100644 classes/authenticator/User.php
create mode 100644 classes/authenticator/UserInterface.php
create mode 100644 classes/authenticator/UserManager.php
create mode 100644 classes/authenticator/UserManagerInterface.php
create mode 100644 classes/authenticator/UserToken.php
create mode 100644 classes/authenticator/UserTokenInterface.php
create mode 100644 classes/authenticator/info.md
create mode 100644 classes/cache/Cache_interface.php
create mode 100644 classes/cache/FileCache.php
create mode 100644 classes/cache/MemcachedCache.php
create mode 100644 classes/cache/RedisCache.php
create mode 100644 classes/session/DatabaseSession.php
create mode 100644 classes/session/DefaultSession.php
create mode 100644 classes/session/FilesSession.php
create mode 100644 classes/session/SessionHandlerInterface.php
create mode 100644 cli/micro/setup.php
create mode 100644 cli/micro/term_utilities.php
create mode 100644 config/anom_settings.php
create mode 100644 config/constants.php
create mode 100644 config/credentials.php
create mode 100644 config/init.php
create mode 100644 helpers/autoload.php
create mode 100644 helpers/design_patterns.php
create mode 100644 helpers/error_handling.php
create mode 100644 helpers/render.php
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0b728f0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,72 @@
+# commonly ignored file patterns.
+# ---------------------------------------------
+
+# -- Node artifact files
+node_modules/
+# dist/
+
+# -- Compiled Java class files
+# *.class
+
+# -- Compiled Python bytecode
+# *.py[cod]
+
+# -- Log files
+*.log
+
+# -- Package files
+# *.jar
+
+# -- Maven
+# target/
+# dist/
+
+# -- JetBrains IDE
+.idea/
+
+# -- Unit test reports
+# TEST*.xml
+
+# -- Generated by MacOS
+.DS_Store
+
+# -- Generated by Windows
+Thumbs.db
+
+# -- Applications
+*.app
+*.exe
+*.war
+
+# Large media files
+*.mp4
+*.tiff
+*.avi
+*.flv
+*.mov
+*.wmv
+
+
+# x-anom gitignore rules
+# ---------------------------------------------
+
+# -- enviriment-variables
+.env
+
+# -- data files of data/ folder
+data/*
+
+# -- local cache
+html/cache/*
+
+# -- exceptions: keep proposing directory-structure
+!data/.gitkeep
+!core/auth/.gitkeep
+!html/cache/.gitkeep
+
+# -- ignore vendor files
+vendor/*
+
+# -- vscode ide configurations
+.vscode
+.vscode/*
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb92086
--- /dev/null
+++ b/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
+ |
+ |-- html ** 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/auth/.gitkeep b/auth/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/classes/Benchmark.php b/classes/Benchmark.php
new file mode 100644
index 0000000..a7e93ea
--- /dev/null
+++ b/classes/Benchmark.php
@@ -0,0 +1,150 @@
+';
+ $close = '
';
+ $divider = ":";
+ $prefix = '';
+ $suffix = '
';
+ break;
+ case 'code':
+ $open = '';
+ $close = '';
+ $divider = ": ";
+ $prefix = '';
+ $suffix = '
';
+ break;
+ case 'comment':
+ default:
+ $open = '';
+ $divider = ":";
+ $prefix = '';
+ $suffix = '';
+ break;
+ }
+
+ echo $prefix;
+
+ // TIME REPORT
+ // ---------------------------------------------------------------------
+ echo "\n\n{$open} Timings {$close}";
+
+ // valid from php 7.3
+ // $start = self::$timeSpots[ array_key_first(self::$timespots) ];
+ // $end = self::$timeSpots[ array_key_last(self::$timespots) ];
+ $start = self::$timeSpots['start'];
+ $end = self::$timeSpots['end'];
+ $all_dt = number_format($end - $start , 4)*1000 ." ms";
+
+
+
+ // if more than 2 timespots
+ // echo dt between each spot
+ if (count(self::$timeSpots) > 2) {
+
+ // calculate dt between spots
+ $time_results = array();
+ $prev_key = '';
+ $prev_time = 0;
+ foreach(self::$timeSpots as $key => $t) {
+ if (!$prev_time) {
+ $prev_time = $t;
+ $prev_key = $key;
+ }
+ else {
+ $dt = number_format($t - $prev_time , 4)*1000 ." ms";
+ $time_results[] = array(
+ 'part' => "{$prev_key}[..{$key}]", ///$key,
+ 'dt' => $dt
+ );
+ $prev_key = $key; //// "{$prev_key}[..{$key}]";
+ $prev_time = $t;
+ }
+ }
+
+ // echo timings
+ foreach ($time_results as $key => $val) {
+ echo "\n\t{$open} {$val['part']} {$divider} {$val['dt']} {$close}";
+ }
+ }
+
+ // echo total time
+ echo "\n\t{$open} total {$divider} {$all_dt} {$close}";
+
+
+ // MEMORY REPORT
+ // ---------------------------------------------------------------------
+ echo "\n\n{$open} Memory Usage {$close}";
+
+ if (count(self::$memoryUse) > 2) {
+
+ foreach (self::$memoryUse as $key => $val) {
+ echo "\n\t{$open} {$key} {$divider} {$val}MB {$close}";
+ }
+
+ } else {
+ echo "\n\t{$open} memory usage {$divider} ". round(memory_get_usage()/(1024*1024),2) ."MB {$close}";
+ }
+
+ echo $suffix;
+
+ }
+
+ }
+}
diff --git a/classes/Cart.php b/classes/Cart.php
new file mode 100644
index 0000000..4e6f585
--- /dev/null
+++ b/classes/Cart.php
@@ -0,0 +1,71 @@
+uid], 10)
+ //
+ // oranize in results = [
+ // {
+ // oid,
+ // prods: [
+ // {
+ // pid,
+ // prodlabel,
+ // count
+ // },
+ // ...
+ // ]
+ // },
+ // ...,
+ // ]
+ //
+ // return $results
+
+ }
+}
+
+
+
+/* ---
+orders:
+ : id
+ : uid (user id)
+
+order_products
+ : id
+ : oid (order id)
+ : pid (product id)
+ -- */
\ No newline at end of file
diff --git a/classes/Database.php b/classes/Database.php
new file mode 100644
index 0000000..35ff65f
--- /dev/null
+++ b/classes/Database.php
@@ -0,0 +1,331 @@
+throw_errors = PRODUCTION ? false : true;
+
+ if (null == $this->connection) {
+ try {
+ $this->connection = new PDO(
+ "mysql:" . PDO_HOST . ";" . "dbname=" . DB_NAME,
+ DB_USER,
+ DB_PASS,
+ array(
+ PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
+ PDO::MYSQL_ATTR_LOCAL_INFILE => true
+ )
+ );
+
+ // handle error reporting
+ if ($this->throw_errors) {
+ $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+ $this->setTimezone();
+
+ } catch (PDOException $exc) { handle_exception($exc); }
+ }
+ # return $this->connection;
+ }
+
+
+ /** disconnect
+ * CHECK: not sure if needed
+ */
+ public function disconnect()
+ {
+ $this->connection = null;
+ }
+
+
+ /** set Timezone
+ * (Self-explanatory)
+ */
+ public function setTimezone($timezone = DB_TIMEZONE) {
+ // $this->connection->prepare($timezone)->execute();
+ }
+
+
+ /** lastInsertID
+ * returns the ID of last inserted record
+ */
+ public function lastInsertID()
+ {
+ return $this->connection->insert_id;
+ }
+
+
+ /** query
+ * ---
+ * set and execute a query safely;
+ * save results as associative array; DO NOT RETURN RESULTS
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $pypass (boolean): flag to bypass security check
+ * @return $this (database handler)
+ */
+ public function query($sql, $args=[])
+ {
+ try {
+
+ $stmt = $this->connection->prepare($sql);
+
+ if ($args == []) {
+ $result = $stmt->execute();
+
+ } else {
+ $result = $stmt->execute($args);
+
+ }
+
+ $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $this->result = $result;
+
+ return $this;
+
+ } catch (PDOException $exc) { handle_exception($exc); }
+
+ }
+
+
+ /** getAll
+ * ---
+ * return all resulted records
+ * use it after db->query();
+ */
+ public function getAll()
+ {
+ return ($this->result === null) ? false : $this->result;
+ }
+
+
+ /** getFirst
+ * ---
+ * get first row of the resulted query;
+ * used when one row is expected
+ * ex. $db->query('SELECT * FROM users WHERE id = :id',['id'=>1])->getFirst();
+ */
+ public function getFirst()
+ {
+ if (($this->result === null) || ($this->result == [])) {
+ return false;
+
+ } else { return $this->result[0]; }
+ }
+
+
+ /** getOnly
+ * ---
+ * return the first column value of the first row
+ * used when only one value is needed
+ * ex. $db->query('SELECT Count(id) FROM table',[])->getOnly();
+ */
+ public function getOnly()
+ {
+ if (($this->result === null) || ($this->result == [])) {
+ return false;
+
+ } else { return array_values($this->result[0])[0]; }
+ }
+
+
+ /** runQuery
+ * --- (shortcut method)
+ * execute a query safely;
+ * return results as associative array
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $pypass (boolean): flag to bypass security check
+ */
+ public function runQuery($sql, $args=[])
+ {
+ return $this->query($sql, $args)->getAll();
+ }
+
+
+ /** runLimitQuery( sql, args, limit=100, offset = null )
+ * set LIMIT / OFFSET clauses in a secure way
+ * @param $sql (string): SQL query
+ * @param $args (array): array of values to bind into SQL
+ * @param $limit (int): LIMIT number
+ * @param $offset (int): OFFSET number
+ */
+ public function runLimitQuery($sql, $args, $limit = 100, $offset = null)
+ {
+ $limitStr = $offsetStr = "";
+
+ // construct LIMIT clause
+ if (is_int($limit)) {
+ $limitStr = " LIMIT {$limit}";
+
+ // construct OFFSET clause (when a LIMIT pre-exists)
+ if (is_int($offset)) {
+ $offsetStr =" OFFSET {$offset}";
+ }
+ }
+
+ $sql = $sql . $limitStr . $offsetStr;
+
+ return $this->runQuery($sql, $args);
+ }
+
+
+ /** insert
+ * @param $table (string): name of table
+ * @param $values: an associative of (fieldName => value) pairs
+ *
+ * example call:
+ * ---
+ * $db->insert('products',
+ * [
+ * 'title' => 'My Dark Chocolate 200g',
+ * 'text' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit ...',
+ * 'isFood' => 1,
+ * 'isToxic' => 0
+ * ]
+ * );
+ *
+ * ...which prepares the SQL query:
+ * INSERT INTO products (title, text, isFood, isToxic)
+ * VALUES (:title, :text, :isFood, :isToxic)
+ *
+ * ...and injects the values: [ :title => 'My Dark Chocolate 200g' , ... ]
+ */
+ public function insert($table, array $values)
+ {
+ $fieldSets = [];
+ $valueSets = [];
+ $bindSets = [];
+
+ foreach($values as $key => $val) {
+ $fieldSets[] = $key;
+ $valueSets[] =':'. $key;
+ $bindSets[':'. $key] = $val;
+ }
+
+ $sql = "INSERT INTO {$table} (". implode(', ', $fieldSets) .")
+ VALUES (". implode(', ', $valueSets) .")";
+
+ return $this->runQuery($sql, $bindSets);
+ }
+
+
+ /** update
+ * @param $table (string): name of table
+ * @param $values: an associative of (fieldName => value) pairs
+ * @param $id: an associative of (fieldName => value) index fields
+ *
+ * example call:
+ * ---
+ * $db->update('products',
+ * [ 'title' => 'My Chocolate','isFood' => 1 ],
+ * [ 'id' => 123 ]
+ * );
+ *
+ * ...which prepares the SQL query:
+ * UPDATE products SET `title` = :title, `isFood` = :isFood WHERE id = :id
+ *
+ * ...and injects: [':title'=> 'My Chocolate' , ':isFood'=> 1 , ':id'=> 123]
+ */
+ public function update( $table, array $values, array $identity)
+ {
+ $fieldSets = []; // array of field names
+ $idSets = []; // array of data-holders
+ $bindSets = []; // array of data-bindings
+
+ foreach($values as $key => $val) {
+ $fieldSets[] = "{$key} = :{$key}";
+ $bindSets[':'. $key] = $val;
+ }
+
+ foreach($identity as $key => $val) {
+ $idSets = "{$key} = :{$key}";
+ $bindSets[':'. $key] = $val;
+ }
+
+ $sql = "UPDATE {$table} SET ". implode(', ', $fieldSets)
+ ." WHERE ". implode(" AND ", $idSets);
+
+ return $this->runQuery($sql, $bindsArray);
+ }
+
+
+ /** multiInsert( table, fields , values )
+ * Construct a multiple-insert clause
+ *
+ * @param $table (string): name of table
+ * @param $fields (array): array with field-names
+ * @param $values (array): array of value-arrays
+ *
+ * example call:
+ * ---
+ * $db->multiInsert('order_products',
+ * [ 'orderID', 'productID', 'unitPrice', 'quantity', 'note' ],
+ * [
+ * [ 124, 102030, 1.25, 5, '' ],
+ * [ 124, 102040, 10.50, 2, 'some note about product #102040' ],
+ * [ 124, 102050, 7.20, 3, '' ],
+ * [ 124, 102060, 3.25, 1, 'some other note' ]
+ * ]
+ * );
+ */
+ public function multiInsert($table, array $fieldsArray, array $valuesArray)
+ {
+ if (count($fieldsArray) != count($valuesArray[0])) {
+ throw new Exception('Fields and value arrays don\'t match.');
+ }
+
+ // setup fieldsSet
+ // ex. "(Title, Price, Status)"
+ $fieldsSet = ' (`'. implode(
+ '`, `', // make sure fieldnames are not SQL-bound terms
+ str_replace('`', '', $fieldsArray) // clean fieldnames
+ ) .'`) ';
+
+ // setup holders array and bind-values array
+ // ex. "(:Title1, :Price1, :Status1), (:Title2, :Price2, :Status2), ...",
+ $holdersArray = [];
+ $bindsArray = [];
+ $counter = 1;
+ foreach($valuesArray as $key => $rowArray) {
+ $rowHolders = [];
+
+ foreach($itemArray as $key => $val) {
+ $rowHolders = ':'. $fieldsArray[$key] . $counter;
+ $bindsArray[ ':'. $fieldsArray[$key] . $counter ] = $val;
+ }
+ $holdersArray[] = '('. implode(', ', $rowHolders ) .')';
+ $counter++;
+ }
+
+ $sql = "INSERT INTO {$table}" . $fieldsSet
+ . ' VALUES '. impload(', ', $holdersArray);
+
+ return $this->runQuery($sql, $bindsArray);
+ }
+
+}
diff --git a/classes/Registry.php b/classes/Registry.php
new file mode 100644
index 0000000..df012e9
--- /dev/null
+++ b/classes/Registry.php
@@ -0,0 +1,129 @@
+URL = $_SERVER['REQUEST_URI'];
+ $this->PATH = (!empty($parsed['path'])) ? urldecode($parsed['path']) : '';
+ $this->QUERY = (!empty($parsed['query'])) ? urldecode($parsed['query']) : false;
+ $this->HOST = $_SERVER['HTTP_HOST'];
+ $this->PORT = $_SERVER['SERVER_PORT'];
+ $this->TIME = $_SERVER['REQUEST_TIME'];
+ $this->CLI_IP = $_SERVER['REMOTE_ADDR'];
+
+ $this->METHOD = strtolower($_SERVER['REQUEST_METHOD']);
+
+ $this->GET = $_GET; // $_GET should only used
+ // to request data or specify options (never to perform
+ // system-changes) thus should not need any validation;
+ // * If (for any reason) you requide $_GET sanitization
+ // enable it later on the method's code
+
+ // $this->INTERFACE = php_sapi_name();
+
+ $this->AGENT = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
+ $this->SIGNATURE = sha1(
+ $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
+ . $_SERVER['HTTP_ACCEPT'] ?? ''
+ . $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''
+ . $_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''
+ );
+
+ // sanitize user input
+ // if (isset($_GET)) { $this->GET = $this->sanitize($_GET); }
+ $this->GET = $this->sanitize($_GET);
+ if (isset($_POST)) { $this->POST = $this->sanitize($_POST); }
+ if (isset($_COOKIE)) { $this->COOKIE = $this->sanitize($_COOKIE); }
+
+ // check anti-CSRF token if needed
+ // (again, GET requests should not need CSRF cheking)
+ if (in_array($this->METHOD, ['post', 'put', 'patch', 'delete'])) {
+ // TODO: only if CSRF protection enabled...
+ $this->checkCsrfToken();
+ }
+
+ }
+
+
+ private function sanitize($array)
+ {
+ // TODO:
+ // ...
+ return $array;
+ }
+
+
+ public function checkCsrfToken()
+ {
+ // TODO:
+ // ...
+ // if SCRF-token is not valideted, serve 403
+ return $array;
+ }
+
+}
+
diff --git a/classes/Route.php b/classes/Route.php
new file mode 100644
index 0000000..072723c
--- /dev/null
+++ b/classes/Route.php
@@ -0,0 +1,97 @@
+ $expression,
+ 'function' => $function,
+ 'method' => strtolower($method)
+ ));
+ }
+
+
+ /** notFound($function)
+ * ---
+ * @param $function : call back function to be executed
+ */
+ public static function notFound($function)
+ {
+ self::$notFound = $function;
+ }
+
+
+ /** run()
+ * ---
+ * Parse request ; Find mathing route ;
+ * then call route's function
+ * usualy a Controller::method([poarametres])
+ */
+ public static function run(Request $request)
+ {
+ // $request = Registry::get('REQUEST');
+ $path = $request->PATH; // request path
+ $method = $request->METHOD; // request method
+
+ $path_match_found = false;
+ $route_match_found = false;
+
+ foreach(self::$routes as $route) {
+
+ // If method matched check the path
+ if ($route['method'] == $method || $method == 'any') {
+
+ // Add 'find string start' automatically
+ $route['expression'] = '^'.$route['expression'];
+
+ // Add 'find string end' automatically
+ $route['expression'] = $route['expression'].'$';
+
+ // Check path match
+ if (preg_match('#'. $route['expression'] .'#', $path, $matches)) {
+
+ $route_match_found = true;
+
+ array_shift($matches); // Always remove first element. This contains the whole string
+
+ call_user_func_array($route['function'], $matches);
+ break; // Do not check other routes
+
+ }
+ }
+ }
+
+ // No matching route was found
+ if (!$route_match_found) {
+ header("HTTP/1.0 404 Not Found");
+ if (self::$notFound) {
+ call_user_func_array(self::$notFound, []);
+ }
+ }
+
+ }
+
+}
diff --git a/classes/Security.php b/classes/Security.php
new file mode 100644
index 0000000..61979db
--- /dev/null
+++ b/classes/Security.php
@@ -0,0 +1,923 @@
+', '<', '>',
+ "'", '"', '&', '$', '#',
+ '{', '}', '[', ']', '=',
+ ';', '?', '%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]',
+ '' => '-->',
+ ' '<![CDATA[',
+ '' => '<comment>',
+ '<%' => '<%'
+ );
+
+ // 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:
+ // Google
+ // 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:
+ // '), array('<?', '?>'), $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('/]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
+ }
+
+ if (preg_match('/
]*?)(?:\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: