summaryrefslogtreecommitdiff
path: root/app/Core
diff options
context:
space:
mode:
Diffstat (limited to 'app/Core')
-rw-r--r--app/Core/Event.php135
-rw-r--r--app/Core/Listener.php17
-rw-r--r--app/Core/Loader.php37
-rw-r--r--app/Core/Registry.php79
-rw-r--r--app/Core/Request.php56
-rw-r--r--app/Core/Response.php138
-rw-r--r--app/Core/Router.php111
-rw-r--r--app/Core/Session.php56
-rw-r--r--app/Core/Template.php72
-rw-r--r--app/Core/Translator.php155
10 files changed, 856 insertions, 0 deletions
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;
+ }
+ }
+}