From 059e0d95d0c28bc5060e87e146eaf7411f51bf90 Mon Sep 17 00:00:00 2001
From: George Halkiadakis
Date: Thu, 27 Apr 2023 03:47:30 +0300
Subject: skeleton commit; based on an anom project
---
core/README.md | 266 ++++++
core/auth/.gitkeep | 0
core/auth/env.example | 36 +
core/classes/Benchmark.php | 150 ++++
core/classes/Cart.php | 71 ++
core/classes/Database.php | 333 ++++++++
core/classes/Registry.php | 135 +++
core/classes/Render.php | 358 ++++++++
core/classes/Repository.php | 43 +
core/classes/Request.php | 161 ++++
core/classes/Route.php | 97 +++
core/classes/Security.php | 923 +++++++++++++++++++++
core/classes/authentication/Core/PasswordTrait.php | 38 +
core/classes/authentication/Core/UserManager.php | 101 +++
.../authentication/Core/UserManagerInterface.php | 25 +
core/classes/authentication/Token/UserToken.php | 44 +
.../authentication/Token/UserTokenInterface.php | 18 +
core/classes/authentication/User.php | 116 +++
core/classes/authentication/UserInterface.php | 19 +
core/classes/authentication/info.md | 114 +++
core/classes/cache/Cache_interface.php | 34 +
core/classes/cache/FileCache.php | 93 +++
core/classes/cache/MemcachedCache.php | 37 +
core/classes/cache/RedisCache.php | 69 ++
core/classes/session/DatabaseSession.php | 138 +++
core/classes/session/DefaultSession.php | 42 +
core/classes/session/FilesSession.php | 138 +++
core/classes/session/SessionHandlerInterface.php | 169 ++++
core/cli/cli-cache.php | 65 ++
core/cli/curl-cache.php | 155 ++++
core/cli/info.md | 23 +
core/cli/micro/setup.php | 120 +++
core/cli/micro/term_utilities.php | 44 +
core/config/anom_settings.php | 325 ++++++++
core/config/constants.php | 28 +
core/config/init.php | 64 ++
core/helpers/autoload.php | 43 +
core/helpers/design_patterns.php | 104 +++
core/helpers/error_handling.php | 119 +++
39 files changed, 4858 insertions(+)
create mode 100644 core/README.md
create mode 100644 core/auth/.gitkeep
create mode 100644 core/auth/env.example
create mode 100644 core/classes/Benchmark.php
create mode 100644 core/classes/Cart.php
create mode 100644 core/classes/Database.php
create mode 100644 core/classes/Registry.php
create mode 100644 core/classes/Render.php
create mode 100644 core/classes/Repository.php
create mode 100644 core/classes/Request.php
create mode 100644 core/classes/Route.php
create mode 100644 core/classes/Security.php
create mode 100644 core/classes/authentication/Core/PasswordTrait.php
create mode 100644 core/classes/authentication/Core/UserManager.php
create mode 100644 core/classes/authentication/Core/UserManagerInterface.php
create mode 100644 core/classes/authentication/Token/UserToken.php
create mode 100644 core/classes/authentication/Token/UserTokenInterface.php
create mode 100644 core/classes/authentication/User.php
create mode 100644 core/classes/authentication/UserInterface.php
create mode 100644 core/classes/authentication/info.md
create mode 100644 core/classes/cache/Cache_interface.php
create mode 100644 core/classes/cache/FileCache.php
create mode 100644 core/classes/cache/MemcachedCache.php
create mode 100644 core/classes/cache/RedisCache.php
create mode 100644 core/classes/session/DatabaseSession.php
create mode 100644 core/classes/session/DefaultSession.php
create mode 100644 core/classes/session/FilesSession.php
create mode 100644 core/classes/session/SessionHandlerInterface.php
create mode 100644 core/cli/cli-cache.php
create mode 100755 core/cli/curl-cache.php
create mode 100644 core/cli/info.md
create mode 100644 core/cli/micro/setup.php
create mode 100644 core/cli/micro/term_utilities.php
create mode 100644 core/config/anom_settings.php
create mode 100644 core/config/constants.php
create mode 100644 core/config/init.php
create mode 100644 core/helpers/autoload.php
create mode 100644 core/helpers/design_patterns.php
create mode 100644 core/helpers/error_handling.php
(limited to 'core')
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
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 @@
+';
+ $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/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 @@
+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 @@
+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 @@
+";
+ }
+
+ }
+
+ /** 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 "";
+ }
+
+ }
+
+
+ /** 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";
+ }
+ break;
+
+ case 'js':
+ foreach($assetIDs as $asset) {
+ $code .= "\n\t";
+ }
+ break;
+
+ case 'css':
+ default:
+ foreach($assetIDs as $asset) {
+ $code .= "\n\t";
+ }
+ }
+
+ 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 @@
+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 @@
+ $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 @@
+', '<', '>',
+ "'", '"', '&', '$', '#',
+ '{', '}', '[', ']', '=',
+ ';', '?', '%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: