From 2230dd4e6b148346c0ec596b9e3e12996a762ed8 Mon Sep 17 00:00:00 2001
From: Frédéric Guillot <fred@kanboard.net>
Date: Thu, 22 May 2014 12:28:28 -0400
Subject: Code refactoring (add autoloader and change files organization)

---
 app/Core/Event.php      | 135 +++++++++++++++++++++++++++++++++++++++++
 app/Core/Listener.php   |  17 ++++++
 app/Core/Loader.php     |  37 ++++++++++++
 app/Core/Registry.php   |  79 ++++++++++++++++++++++++
 app/Core/Request.php    |  56 +++++++++++++++++
 app/Core/Response.php   | 138 ++++++++++++++++++++++++++++++++++++++++++
 app/Core/Router.php     | 111 ++++++++++++++++++++++++++++++++++
 app/Core/Session.php    |  56 +++++++++++++++++
 app/Core/Template.php   |  72 ++++++++++++++++++++++
 app/Core/Translator.php | 155 ++++++++++++++++++++++++++++++++++++++++++++++++
 10 files changed, 856 insertions(+)
 create mode 100644 app/Core/Event.php
 create mode 100644 app/Core/Listener.php
 create mode 100644 app/Core/Loader.php
 create mode 100644 app/Core/Registry.php
 create mode 100644 app/Core/Request.php
 create mode 100644 app/Core/Response.php
 create mode 100644 app/Core/Router.php
 create mode 100644 app/Core/Session.php
 create mode 100644 app/Core/Template.php
 create mode 100644 app/Core/Translator.php

(limited to 'app/Core')

diff --git a/app/Core/Event.php b/app/Core/Event.php
new file mode 100644
index 00000000..2c029b49
--- /dev/null
+++ b/app/Core/Event.php
@@ -0,0 +1,135 @@
+<?php
+
+namespace Core;
+
+/**
+ * Event dispatcher class
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Event
+{
+    /**
+     * Contains all listeners
+     *
+     * @access private
+     * @var array
+     */
+    private $listeners = array();
+
+    /**
+     * The last listener executed
+     *
+     * @access private
+     * @var string
+     */
+    private $lastListener = '';
+
+    /**
+     * The last triggered event
+     *
+     * @access private
+     * @var string
+     */
+    private $lastEvent = '';
+
+    /**
+     * Triggered events list
+     *
+     * @access private
+     * @var array
+     */
+    private $events = array();
+
+    /**
+     * Attach a listener object to an event
+     *
+     * @access public
+     * @param  string   $eventName   Event name
+     * @param  Listener $listener    Object that implements the Listener interface
+     */
+    public function attach($eventName, Listener $listener)
+    {
+        if (! isset($this->listeners[$eventName])) {
+            $this->listeners[$eventName] = array();
+        }
+
+        $this->listeners[$eventName][] = $listener;
+    }
+
+    /**
+     * Trigger an event
+     *
+     * @access public
+     * @param  string   $eventName   Event name
+     * @param  array    $data        Event data
+     */
+    public function trigger($eventName, array $data)
+    {
+        $this->lastEvent = $eventName;
+        $this->events[] = $eventName;
+
+        if (isset($this->listeners[$eventName])) {
+            foreach ($this->listeners[$eventName] as $listener) {
+                if ($listener->execute($data)) {
+                    $this->lastListener = get_class($listener);
+                }
+            }
+        }
+    }
+
+    /**
+     * Get the last listener executed
+     *
+     * @access public
+     * @return string Event name
+     */
+    public function getLastListenerExecuted()
+    {
+        return $this->lastListener;
+    }
+
+    /**
+     * Get the last fired event
+     *
+     * @access public
+     * @return string Event name
+     */
+    public function getLastTriggeredEvent()
+    {
+        return $this->lastEvent;
+    }
+
+    /**
+     * Get a list of triggered events
+     *
+     * @access public
+     * @return array
+     */
+    public function getTriggeredEvents()
+    {
+        return $this->events;
+    }
+
+    /**
+     * Check if a listener bind to an event
+     *
+     * @access public
+     * @param  string $eventName    Event name
+     * @param  mixed  $instance     Instance name or object itself
+     * @return bool                 Yes or no
+     */
+    public function hasListener($eventName, $instance)
+    {
+        if (isset($this->listeners[$eventName])) {
+            foreach ($this->listeners[$eventName] as $listener) {
+                if ($listener instanceof $instance) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/app/Core/Listener.php b/app/Core/Listener.php
new file mode 100644
index 00000000..b8bdd680
--- /dev/null
+++ b/app/Core/Listener.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Core;
+
+/**
+ * Event listener interface
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+interface Listener {
+
+    /**
+     * @return boolean
+     */
+    public function execute(array $data);
+}
diff --git a/app/Core/Loader.php b/app/Core/Loader.php
new file mode 100644
index 00000000..7c437654
--- /dev/null
+++ b/app/Core/Loader.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Core;
+
+/**
+ * Loader class
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Loader
+{
+    /**
+     * Load the missing class
+     *
+     * @access public
+     * @param  string   $class   Class name
+     */
+    public function load($class)
+    {
+        $filename = __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
+
+        if (file_exists($filename)) {
+            require $filename;
+        }
+    }
+
+    /**
+     * Register the autoloader
+     *
+     * @access public
+     */
+    public function execute()
+    {
+        spl_autoload_register(array($this, 'load'));
+    }
+}
diff --git a/app/Core/Registry.php b/app/Core/Registry.php
new file mode 100644
index 00000000..f11d427c
--- /dev/null
+++ b/app/Core/Registry.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Core;
+
+/**
+ * The registry class is a dependency injection container
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Registry
+{
+    /**
+     * Contains all dependencies
+     *
+     * @access private
+     * @var array
+     */
+    private $container = array();
+
+    /**
+     * Contains all instances
+     *
+     * @access private
+     * @var array
+     */
+    private $instances = array();
+
+    /**
+     * Set a dependency
+     *
+     * @access public
+     * @param  string   $name   Unique identifier for the service/parameter
+     * @param  mixed    $value  The value of the parameter or a closure to define an object
+     */
+    public function __set($name, $value)
+    {
+        $this->container[$name] = $value;
+    }
+
+    /**
+     * Get a dependency
+     *
+     * @access public
+     * @param  string   $name   Unique identifier for the service/parameter
+     * @return mixed            The value of the parameter or an object
+     * @throws RuntimeException If the identifier is not found
+     */
+    public function __get($name)
+    {
+        if (isset($this->container[$name])) {
+
+            if (is_callable($this->container[$name])) {
+                return $this->container[$name]();
+            }
+            else {
+                return $this->container[$name];
+            }
+        }
+
+        throw new \RuntimeException('Identifier not found in the registry: '.$name);
+    }
+
+    /**
+     * Return a shared instance of a dependency
+     *
+     * @access public
+     * @param  string   $name   Unique identifier for the service/parameter
+     * @return mixed            Same object instance of the dependency
+     */
+    public function shared($name)
+    {
+        if (! isset($this->instances[$name])) {
+            $this->instances[$name] = $this->$name;
+        }
+
+        return $this->instances[$name];
+    }
+}
diff --git a/app/Core/Request.php b/app/Core/Request.php
new file mode 100644
index 00000000..df8ea41a
--- /dev/null
+++ b/app/Core/Request.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Core;
+
+class Request
+{
+    public function getStringParam($name, $default_value = '')
+    {
+        return isset($_GET[$name]) ? $_GET[$name] : $default_value;
+    }
+
+    public function getIntegerParam($name, $default_value = 0)
+    {
+        return isset($_GET[$name]) && ctype_digit($_GET[$name]) ? (int) $_GET[$name] : $default_value;
+    }
+
+    public function getValue($name)
+    {
+        $values = $this->getValues();
+        return isset($values[$name]) ? $values[$name] : null;
+    }
+
+    public function getValues()
+    {
+        if (! empty($_POST)) return $_POST;
+
+        $result = json_decode($this->getBody(), true);
+        if ($result) return $result;
+
+        return array();
+    }
+
+    public function getBody()
+    {
+        return file_get_contents('php://input');
+    }
+
+    public function getFileContent($name)
+    {
+        if (isset($_FILES[$name])) {
+            return file_get_contents($_FILES[$name]['tmp_name']);
+        }
+
+        return '';
+    }
+
+    public function isPost()
+    {
+        return isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST';
+    }
+
+    public function isAjax()
+    {
+        return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
+    }
+}
diff --git a/app/Core/Response.php b/app/Core/Response.php
new file mode 100644
index 00000000..a5f0e4dc
--- /dev/null
+++ b/app/Core/Response.php
@@ -0,0 +1,138 @@
+<?php
+
+namespace Core;
+
+class Response
+{
+    public function forceDownload($filename)
+    {
+        header('Content-Disposition: attachment; filename="'.$filename.'"');
+    }
+
+    /**
+     * @param integer $status_code
+     */
+    public function status($status_code)
+    {
+        header('Status: '.$status_code);
+        header($_SERVER['SERVER_PROTOCOL'].' '.$status_code);
+    }
+
+    public function redirect($url)
+    {
+        header('Location: '.$url);
+        exit;
+    }
+
+    public function json(array $data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Type: application/json');
+        echo json_encode($data);
+
+        exit;
+    }
+
+    public function text($data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Type: text/plain; charset=utf-8');
+        echo $data;
+
+        exit;
+    }
+
+    public function html($data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Type: text/html; charset=utf-8');
+        echo $data;
+
+        exit;
+    }
+
+    public function xml($data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Type: text/xml; charset=utf-8');
+        echo $data;
+
+        exit;
+    }
+
+    public function js($data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Type: text/javascript; charset=utf-8');
+        echo $data;
+
+        exit;
+    }
+
+    public function binary($data, $status_code = 200)
+    {
+        $this->status($status_code);
+
+        header('Content-Transfer-Encoding: binary');
+        header('Content-Type: application/octet-stream');
+        echo $data;
+
+        exit;
+    }
+
+    public function csp(array $policies = array())
+    {
+        $policies['default-src'] = "'self'";
+        $values = '';
+
+        foreach ($policies as $policy => $hosts) {
+
+            if (is_array($hosts)) {
+
+                $acl = '';
+
+                foreach ($hosts as &$host) {
+
+                    if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) {
+                        $acl .= $host.' ';
+                    }
+                }
+            }
+            else {
+
+                $acl = $hosts;
+            }
+
+            $values .= $policy.' '.trim($acl).'; ';
+        }
+
+        header('Content-Security-Policy: '.$values);
+    }
+
+    public function nosniff()
+    {
+        header('X-Content-Type-Options: nosniff');
+    }
+
+    public function xss()
+    {
+        header('X-XSS-Protection: 1; mode=block');
+    }
+
+    public function hsts()
+    {
+        if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') {
+            header('Strict-Transport-Security: max-age=31536000');
+        }
+    }
+
+    public function xframe($mode = 'DENY', array $urls = array())
+    {
+        header('X-Frame-Options: '.$mode.' '.implode(' ', $urls));
+    }
+}
diff --git a/app/Core/Router.php b/app/Core/Router.php
new file mode 100644
index 00000000..3a5df715
--- /dev/null
+++ b/app/Core/Router.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Core;
+
+/**
+ * Router class
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Router
+{
+    /**
+     * Controller name
+     *
+     * @access private
+     * @var string
+     */
+    private $controller = '';
+
+    /**
+     * Action name
+     *
+     * @access private
+     * @var string
+     */
+    private $action = '';
+
+    /**
+     * Registry instance
+     *
+     * @access private
+     * @var Core\Registry
+     */
+    private $registry;
+
+    /**
+     * Constructor
+     *
+     * @access public
+     * @param  Core\Registry   $registry     Registry instance
+     * @param  string          $controller   Controller name
+     * @param  string          $action       Action name
+     */
+    public function __construct(Registry $registry, $controller = '', $action = '')
+    {
+        $this->registry = $registry;
+        $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller'];
+        $this->action = empty($_GET['action']) ? $controller : $_GET['action'];
+    }
+
+    /**
+     * Check controller and action parameter
+     *
+     * @access public
+     * @param  string    $value           Controller or action name
+     * @param  string    $default_value   Default value if validation fail
+     */
+    public function sanitize($value, $default_value)
+    {
+        return ! ctype_alpha($value) || empty($value) ? $default_value : strtolower($value);
+    }
+
+    /**
+     * Load a controller and execute the action
+     *
+     * @access public
+     * @param  string     $filename     Controller filename
+     * @param  string     $class        Class name
+     * @param  string     $method       Method name
+     */
+    public function load($filename, $class, $method)
+    {
+        if (file_exists($filename)) {
+
+            require $filename;
+
+            if (! method_exists($class, $method)) {
+                return false;
+            }
+
+            $instance = new $class($this->registry);
+            $instance->request = new Request;
+            $instance->response = new Response;
+            $instance->session = new Session;
+            $instance->template = new Template;
+            $instance->beforeAction($this->controller, $this->action);
+            $instance->$method();
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Find a route
+     *
+     * @access public
+     */
+    public function execute()
+    {
+        $this->controller = $this->sanitize($this->controller, 'app');
+        $this->action = $this->sanitize($this->action, 'index');
+        $filename = __DIR__.'/../Controller/'.ucfirst($this->controller).'.php';
+
+        if (! $this->load($filename, '\Controller\\'.$this->controller, $this->action)) {
+            die('Page not found!');
+        }
+    }
+}
diff --git a/app/Core/Session.php b/app/Core/Session.php
new file mode 100644
index 00000000..0c3ec2d9
--- /dev/null
+++ b/app/Core/Session.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Core;
+
+class Session
+{
+    const SESSION_LIFETIME = 86400; // 1 day
+
+    public function open($base_path = '/', $save_path = '')
+    {
+        if ($save_path !== '') session_save_path($save_path);
+
+        // HttpOnly and secure flags for session cookie
+        session_set_cookie_params(
+            self::SESSION_LIFETIME,
+            $base_path ?: '/',
+            null,
+            isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
+            true
+        );
+
+        // Avoid session id in the URL
+        ini_set('session.use_only_cookies', '1');
+
+        // Ensure session ID integrity
+        ini_set('session.entropy_file', '/dev/urandom');
+        ini_set('session.entropy_length', '32');
+        ini_set('session.hash_bits_per_character', 6);
+
+        // Custom session name
+        session_name('__S');
+
+        session_start();
+
+        // Regenerate the session id to avoid session fixation issue
+        if (empty($_SESSION['__validated'])) {
+            session_regenerate_id(true);
+            $_SESSION['__validated'] = 1;
+        }
+    }
+
+    public function close()
+    {
+        session_destroy();
+    }
+
+    public function flash($message)
+    {
+        $_SESSION['flash_message'] = $message;
+    }
+
+    public function flashError($message)
+    {
+        $_SESSION['flash_error_message'] = $message;
+    }
+}
diff --git a/app/Core/Template.php b/app/Core/Template.php
new file mode 100644
index 00000000..8740a685
--- /dev/null
+++ b/app/Core/Template.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace Core;
+
+/**
+ * Template class
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Template
+{
+    /**
+     * Template path
+     *
+     * @var string
+     */
+    const PATH = 'app/Templates/';
+
+    /**
+     * Load a template
+     *
+     * Example:
+     *
+     * $template->load('template_name', ['bla' => 'value']);
+     *
+     * @access public
+     * @return string
+     */
+    public function load()
+    {
+        if (func_num_args() < 1 || func_num_args() > 2) {
+            die('Invalid template arguments');
+        }
+
+        if (! file_exists(self::PATH.func_get_arg(0).'.php')) {
+            die('Unable to load the template: "'.func_get_arg(0).'"');
+        }
+
+        if (func_num_args() === 2) {
+
+            if (! is_array(func_get_arg(1))) {
+                die('Template variables must be an array');
+            }
+
+            extract(func_get_arg(1));
+        }
+
+        ob_start();
+
+        include self::PATH.func_get_arg(0).'.php';
+
+        return ob_get_clean();
+    }
+
+    /**
+     * Render a page layout
+     *
+     * @access public
+     * @param  string   $template_name   Template name
+     * @param  array    $template_args   Key/value map
+     * @param  string   $layout_name     Layout name
+     * @return string
+     */
+    public function layout($template_name, array $template_args = array(), $layout_name = 'layout')
+    {
+        return $this->load(
+            $layout_name,
+            $template_args + array('content_for_layout' => $this->load($template_name, $template_args))
+        );
+    }
+}
diff --git a/app/Core/Translator.php b/app/Core/Translator.php
new file mode 100644
index 00000000..be0be66a
--- /dev/null
+++ b/app/Core/Translator.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Core;
+
+/**
+ * Translator class
+ *
+ * @package core
+ * @author  Frederic Guillot
+ */
+class Translator
+{
+    /**
+     * Locales path
+     *
+     * @var string
+     */
+    const PATH = 'app/Locales/';
+
+    /**
+     * Locales
+     *
+     * @static
+     * @access private
+     * @var array
+     */
+    private static $locales = array();
+
+    /**
+     * Get a translation
+     *
+     * $translator->translate('I have %d kids', 5);
+     *
+     * @access public
+     * @return string
+     */
+    public function translate($identifier)
+    {
+        $args = func_get_args();
+
+        array_shift($args);
+        array_unshift($args, $this->get($identifier, $identifier));
+
+        foreach ($args as &$arg) {
+            $arg = htmlspecialchars($arg, ENT_QUOTES, 'UTF-8', false);
+        }
+
+        return call_user_func_array(
+            'sprintf',
+            $args
+        );
+    }
+
+    /**
+     * Get a formatted number
+     *
+     * $translator->number(1234.56);
+     *
+     * @access public
+     * @param  float    $number   Number to format
+     * @return string
+     */
+    public function number($number)
+    {
+        return number_format(
+            $number,
+            $this->get('number.decimals', 2),
+            $this->get('number.decimals_separator', '.'),
+            $this->get('number.thousands_separator', ',')
+        );
+    }
+
+    /**
+     * Get a formatted currency number
+     *
+     * $translator->currency(1234.56);
+     *
+     * @access public
+     * @param  float    $amount   Number to format
+     * @return string
+     */
+    public function currency($amount)
+    {
+        $position = $this->get('currency.position', 'before');
+        $symbol = $this->get('currency.symbol', '$');
+        $str = '';
+
+        if ($position === 'before') {
+            $str .= $symbol;
+        }
+
+        $str .= $this->number($amount);
+
+        if ($position === 'after') {
+            $str .= ' '.$symbol;
+        }
+
+        return $str;
+    }
+
+    /**
+     * Get a formatted datetime
+     *
+     * $translator->datetime('%Y-%m-%d', time());
+     *
+     * @access public
+     * @param  string   $format      Format defined by the strftime function
+     * @param  integer  $timestamp   Unix timestamp
+     * @return string
+     */
+    public function datetime($format, $timestamp)
+    {
+        if (! $timestamp) {
+            return '';
+        }
+
+        return strftime($this->get($format, $format), (int) $timestamp);
+    }
+
+    /**
+     * Get an identifier from the translations or return the default
+     *
+     * @access public
+     * @param  string   $idendifier   Locale identifier
+     * @param  string   $default      Default value
+     * @return string
+     */
+    public function get($identifier, $default = '')
+    {
+        if (isset(self::$locales[$identifier])) {
+            return self::$locales[$identifier];
+        }
+        else {
+            return $default;
+        }
+    }
+
+    /**
+     * Load translations
+     *
+     * @static
+     * @access public
+     * @param  string   $language   Locale code: fr_FR
+     */
+    public static function load($language)
+    {
+        setlocale(LC_TIME, $language.'.UTF-8', $language);
+
+        $filename = self::PATH.$language.DIRECTORY_SEPARATOR.'translations.php';
+
+        if (file_exists($filename)) {
+            self::$locales = require $filename;
+        }
+    }
+}
-- 
cgit v1.2.3