summaryrefslogtreecommitdiff
path: root/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'helpers')
-rw-r--r--helpers/autoload.php43
-rw-r--r--helpers/design_patterns.php104
-rw-r--r--helpers/error_handling.php119
-rw-r--r--helpers/render.php338
4 files changed, 604 insertions, 0 deletions
diff --git a/helpers/autoload.php b/helpers/autoload.php
new file mode 100644
index 0000000..c4359c6
--- /dev/null
+++ b/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/helpers/design_patterns.php b/helpers/design_patterns.php
new file mode 100644
index 0000000..d66ffa9
--- /dev/null
+++ b/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/helpers/error_handling.php b/helpers/error_handling.php
new file mode 100644
index 0000000..bc60968
--- /dev/null
+++ b/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('VIEW_LOADED')) {
+ // throw a prety-error message
+ render_view('error/general', ['message' => 'Please forgive me, I know not what I do']);
+ }
+
+ } else {
+
+ if (!defined('VIEW_LOADED')) {
+ // 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 );
+
diff --git a/helpers/render.php b/helpers/render.php
new file mode 100644
index 0000000..9dcd34a
--- /dev/null
+++ b/helpers/render.php
@@ -0,0 +1,338 @@
+<?php
+/** Rendering System
+ * ---
+ * this is the View part of the MVC framework.
+ * Decided not to make it a Class
+ ***/
+
+
+/** 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('VIEW_LOADED')) define('VIEW_LOADED',1);
+ */
+function load_template($template, $data) {
+
+ $file = VIEWS_DIRECTORY . $template . '.php';
+
+ if (file_exists($file)) {
+ if (!defined('VIEW_LOADED')) define('VIEW_LOADED', 1);
+
+ ob_start();
+
+ extract( 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
+ */
+
+function parse_sections($sections) {
+
+ foreach($sections as $sect) {
+
+ if ($sect['key'] == '') {
+ render_view( $sect['view'], $sect['data'] );
+
+ } else {
+ render_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]
+ */
+function render_view($view, $data, $sanitize = false) {
+
+ $file = VIEWS_DIRECTORY . $view . '.php';
+
+ if (file_exists($file)) {
+ if (!defined('VIEW_LOADED')) define('VIEW_LOADED', 1);
+
+ extract( $sanitize ? 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
+ */
+function render_asap($view, $data, $sanitize = false) {
+ render_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
+ */
+function render_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
+ */
+function reply_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);
+}
+
+
+
+
+// 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']);
+ */
+function link_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)
+ */
+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(remove_invisible_characters($val));
+
+ } else if (is_array($val)) {
+ $output[$key] = sanitize_output($val);
+
+ } else {
+ $output[$key] = $val;
+ }
+ }
+ return $output;
+}
+
+
+/** remove_invisible_characters()
+ * ---
+ * @used by sanitize_output()
+ */
+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)
+ */
+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
+ */
+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);
+ }
+ }
+
+}
+
+
+