From 9828a6754778b8349f719995ec77366a8c3485c9 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Sat, 17 Feb 2018 18:18:58 +0100 Subject: [PATCH] +commit 1 --- autoloader.php | 59 +++ build/api/core/AuthSystem.php | 29 ++ build/api/core/Checker.php | 159 ++++++ build/api/core/Config.php | 138 ++++++ build/api/core/Documentation.php | 50 ++ build/api/core/Loader.php | 53 ++ build/api/core/ModuleFactory.php | 48 ++ build/api/core/Request.php | 617 ++++++++++++++++++++++++ build/api/core/Response.php | 133 +++++ build/database/core/DatabaseDriver.php | 197 ++++++++ build/database/core/Repo.php | 79 +++ build/database/core/Repo_i.php | 18 + build/database/repo/user.php | 250 ++++++++++ build/router/controller/api.php | 56 +++ build/router/controller/page.php | 41 ++ build/router/controller/redirect.php | 34 ++ build/router/core/ControllerFactory.php | 51 ++ build/router/core/Route.php | 100 ++++ build/router/core/Router.php | 333 +++++++++++++ config/database-driver.json | 16 + config/modules.json | 94 ++++ config/routes.json | 34 ++ public_html/index.php | 20 + public_html/view/homepage.php | 3 + 24 files changed, 2612 insertions(+) create mode 100755 autoloader.php create mode 100755 build/api/core/AuthSystem.php create mode 100755 build/api/core/Checker.php create mode 100644 build/api/core/Config.php create mode 100644 build/api/core/Documentation.php create mode 100755 build/api/core/Loader.php create mode 100755 build/api/core/ModuleFactory.php create mode 100755 build/api/core/Request.php create mode 100755 build/api/core/Response.php create mode 100755 build/database/core/DatabaseDriver.php create mode 100644 build/database/core/Repo.php create mode 100644 build/database/core/Repo_i.php create mode 100644 build/database/repo/user.php create mode 100644 build/router/controller/api.php create mode 100644 build/router/controller/page.php create mode 100644 build/router/controller/redirect.php create mode 100644 build/router/core/ControllerFactory.php create mode 100644 build/router/core/Route.php create mode 100644 build/router/core/Router.php create mode 100755 config/database-driver.json create mode 100644 config/modules.json create mode 100644 config/routes.json create mode 100644 public_html/index.php create mode 100644 public_html/view/homepage.php diff --git a/autoloader.php b/autoloader.php new file mode 100755 index 0000000..ca24bd1 --- /dev/null +++ b/autoloader.php @@ -0,0 +1,59 @@ + Data to hash + * @salt Salt to use for hashing + * @pepper Pepper to use for hashing + * + */ + function secure_hash(String $raw, String $salt='2104\'dsa:">AS"D:', String $pepper='3894.234123;\'21'){ + return hash('sha512', $pepper.hash('sha512', $raw.$salt)); + } + + + + /* AUTOLOADER + * + * @className Nom de la classe appelee + * + */ + function autoloader($className){ + $path = ''; + + /* [1] On utilise le namespace pour localiser + ===============================================*/ + // On remplace les '\' par des '/' + $path = str_replace('\\', '/', $className) . '.php'; + $path = __BUILD__.'/'.$path; + + // Si le fichier existe + if( file_exists($path) ) + require_once $path; // on inclue le fichier + + } + + // On definit l'autoloader comme autoloader (obvious) + spl_autoload_register('autoloader', false, true); + + +?> diff --git a/build/api/core/AuthSystem.php b/build/api/core/AuthSystem.php new file mode 100755 index 0000000..67010b1 --- /dev/null +++ b/build/api/core/AuthSystem.php @@ -0,0 +1,29 @@ + Liste des permissions attendues + * + * @return error Erreur associée à la permission (Success/PermissionError/TokenError/etc) + * + */ + public static function permission($expected); + } + +?> diff --git a/build/api/core/Checker.php b/build/api/core/Checker.php new file mode 100755 index 0000000..02d631b --- /dev/null +++ b/build/api/core/Checker.php @@ -0,0 +1,159 @@ + Type que l'on veut verifier + * @value Valeur a verifier + * + * @return match Retourne si oui ou non la valeur @value est du bon type @type + * + */ + public static function run($type, $value){ + $checker = true; + + /* [0] On verifie que $value n'est pas nul + =========================================================*/ + if( is_null($value) ) return false; + + + + /* [1] Si de type VARCHAR(min, max, flags) + =========================================================*/ + if( preg_match('/^varchar\((\d+), ?(\d+)((?:, ?\w+)+)?\)$/', $type, $match) ){ + // On recupere la taille min + $min = (int) $match[1]; + // On recupere la taille max + $max = (int) $match[2]; + + // On recupere le sous-type si défini + $flags = isset($match[3]) ? explode(',', substr($match[3], 1)) : null; + + // On effectue la verification de taille + $lenCheck = $checker && is_string($value) && strlen($value) <= $max && strlen($value) >= $min; + + // On vérifie les FLAGS s'il est donné + if( is_array($flags) ) + foreach( $flags as $flag ) + $lenCheck = $lenCheck && self::run($flag, $value); + + return $lenCheck; + } + + + /* [2] Si de type ARRAY(type_elements) + =========================================================*/ + if( preg_match('/^array<(.+)>$/', $type, $match) ){ + + // Si c'est pas un tableau on retourne une erreur + if( !is_array($value) ) + return false; + + + $elements_type = $match[1]; + + // On verifie le type pour chaque element + foreach($value as $element) + // Si erreur dans au moins 1 element, on retourne que c'est incorrect + if( !self::run($elements_type, trim($element) ) ) + return false; + + // Si aucune erreur, on retourne que tout est bon + return true; + } + + + /* [n] Sinon, tous les autres types definis + =========================================================*/ + switch($type){ + // Quoi que ce soit + case 'mixed': + return $checker && !is_null($value); + break; + + // Entier positif (id dans BDD) + case 'id': + return $checker && is_numeric($value) && $value <= 2147483647 && $value >= 0; + break; + + // String quelconque (peut etre vide) + case 'text': + return $checker && is_string($value); + + // Adresse mail (255 caracteres max) + case 'mail': + return $checker && is_string($value) && strlen($value) <= 50 && preg_match('/^[\w\.-]+@[\w\.-]+\.[a-z]{2,4}$/i', $value); + break; + + // Hash sha1/md5 + case 'hash': + return $checker && is_string($value) && preg_match('/^[\da-f]{128}$/', $value); + break; + + case 'alphanumeric': + return $checker && is_string($value) && preg_match('/^[\w\.-]+$/ui', $value); + break; + + case 'letters': + return $checker && is_string($value) && preg_match('/^[a-z -]+$/i', $value); + break; + + case 'status': + return $checker && is_numeric($value) && floor($value) == $value && $value >= 0 && $value <= 100; + break; + + // Tableau non vide + case 'array': + return $checker && is_array($value) && count($value) > 0; + break; + + // Boolean + case 'boolean': + return $checker && is_bool($value); + break; + + // Objet non vide + case 'object': + return $checker && is_object($value) && count((array) $value) > 0; + break; + + // Chaine JSON (on vérifie via le parser) + case 'json': + return $checker && is_string($value) && json_decode($value, true) !== NULL; + break; + + case 'numeric': + return $checker && (is_numeric($value) || $value == null || $value == 'null'); + break; + + case "float": + return $checker && is_float($value); + break; + + default: + return false; + break; + } + + return $checker; + + } + + + } +?> diff --git a/build/api/core/Config.php b/build/api/core/Config.php new file mode 100644 index 0000000..67a1553 --- /dev/null +++ b/build/api/core/Config.php @@ -0,0 +1,138 @@ + Configuration path + * + ---------------------------------------------------------*/ + private function __construct($path=null){ + + // Set default error + $this->error = new Error(Err::Success); + + + /* (1) Access file content + ---------------------------------------------------------*/ { + + /* (1) Vérification existence fichier config */ + if( !file_exists($path) ) + return $this->error->set(Err::UnreachableResource); + + /* (2) Lecture fichier config */ + $conf = @file_get_contents($path); + + /* (3) Si erreur lecture */ + if( $conf === false ) + return $this->error->set(Err::UnreachableResource); + + /* (4) Parsage json */ + $this->raw = json_decode( $conf, true ); + + /* (5) Gestion de l'erreur de parsage */ + if( $this->raw == null ) + return $this->error->set(Err::ParsingFailed, 'json'); + + } + + + /* (2) Construction de l'index des chemins + ---------------------------------------------------------*/ { + + /* (1) Initialisation */ + $this->index = []; + $ref = [ '/' => array_merge($this->raw) ]; + + + /* (2) Tant qu'il reste des @ref à traiter */ + while( count($ref) > 0 ){ + + /* (2.1) For each ref */ + foreach($ref as $ref_path=>$ref_children){ + + /* (2.2) For each children */ + foreach($ref_children as $path=>$method_or_subpath){ + + /* (2.2.1) If is an HTTP method -> add to index */ + if( in_array($path, self::$allowed_http_methods) ){ + + /* (2.2.1.1) If no index for this path -> add it */ + if( !isset($this->index[$ref_path]) ) + $this->index[$ref_path] = []; + + /* (2.2.1.2) Add the HTTP method definition */ + $this->index[$ref_path][$path] = $method_or_subpath; + + + /* (2.2.2) If a sub path -> add it to next refs to process */ + }else{ + + if( $ref_path == '/' ) + $ref["$ref_path$path"] = $method_or_subpath; + else + $ref["$ref_path/$path"] = $method_or_subpath; + + } + + } + + /* (2.3) In all cases -> remove current from ref */ + unset($ref[$ref_path]); + + } + + } + + } + + } + + + + /* (3) Static singleton 'get-or-create' + * + * @return inst Configuration singleton + * + ---------------------------------------------------------*/ + public static function get(){ + + /* (1) If @inst already exists -> return singleton */ + if( self::$inst instanceof Config ) + return self::$inst; + + /* (2) If @inst not set -> create singleton and return it */ + self::$inst = new self( self::config_path() ); + + return self::$inst; + + } + + + + + + + + + } \ No newline at end of file diff --git a/build/api/core/Documentation.php b/build/api/core/Documentation.php new file mode 100644 index 0000000..693fcb1 --- /dev/null +++ b/build/api/core/Documentation.php @@ -0,0 +1,50 @@ + exit */ + if( !isset(Config::get()->index[$rq->get('id')['path']]) ) + return new Response(new Error(Err::WrongPathModule)); + + /* (2) Local store: configuration for this path */ + $cfg = Config::get()->index[$rq->get('id')['path']]; + + + + $response = new Response(); + $response->append('methods', $cfg); + + return $response; + } + + + + + + + + + + } \ No newline at end of file diff --git a/build/api/core/Loader.php b/build/api/core/Loader.php new file mode 100755 index 0000000..df9da92 --- /dev/null +++ b/build/api/core/Loader.php @@ -0,0 +1,53 @@ + URI + * + * @return outName outDesc + * + ---------------------------------------------------------*/ + public static function remote($uri){ + + /* (1) Fetch HttpRequest correct data + ---------------------------------------------------------*/ + /* (1) Parse HttpRequest data because php doesn't parse it for non-POST HTTP method */ + $httprequest = new HttpRequest(); + + /* (2) For later use -> replace default @_POST global */ + $_POST = $httprequest->POST(); + + /* (3) Get @data from @_POST values */ + $data = $_POST; + + + /* (2) Build request + ---------------------------------------------------------*/ + return new Request($uri, $data); + } + + + + + } \ No newline at end of file diff --git a/build/api/core/ModuleFactory.php b/build/api/core/ModuleFactory.php new file mode 100755 index 0000000..23ab7e5 --- /dev/null +++ b/build/api/core/ModuleFactory.php @@ -0,0 +1,48 @@ + Nom du module + * @arguments [OPTIONNEL] Arguments à passer au constructeur + * + * @return instance Instance du module en question + * + */ + public static function getModule($module, $arguments=[]){ + /* (1) On gère les arguments */ + $arguments = is_array($arguments) ? $arguments : []; + + /* (2) Exception: URI Racine */ + if( $module == '/' ) + $module = '/root'; + + /* (3) On transforme @module en namespace */ + $module_ns = str_replace('/', '\\', $module); + + /* (4) On vérifie que la classe existe */ + if( !file_exists(__BUILD__."/api/module$module.php") ) + return false; + + /* (5) On récupère la classe */ + $class_name = "\\api\\module$module_ns"; + + /* (6) On retourne une instance */ + return new $class_name($arguments); + } + + } diff --git a/build/api/core/Request.php b/build/api/core/Request.php new file mode 100755 index 0000000..ea2a4c0 --- /dev/null +++ b/build/api/core/Request.php @@ -0,0 +1,617 @@ + false ]; + private static $authsystem = null; + + + // Attributs prives utiles (initialisation) + private $id; // chemin extrait de l'URI + private $raw_params; // paramètres reçus + private $params; // paramètres donnés à la fonction + private $options; // options + private $http_method; // methode HTTP appelante + + // Contiendra l'etat de la requete + public $error; + + + /* (0) Constructeur d'une requete de module + * + * @uri URI relative de l'appel + * @param Tableau associatif contenant les parametres utiles au traitement + * @forced_method Méthode demandée (optionnel) + * + * @return instance Instance crée + * + ---------------------------------------------------------*/ + public function __construct($uri=null, $params=null, $forced_method=null){ + + return $this->buildRequestObject($uri, $params, $forced_method); + + } + + + + /* (1) Constructeur d'une requete de module (delegation) + * + * @uri URI relative de l'appel + * @param Tableau associatif contenant les parametres utiles au traitement + * @forced_method Méthode demandée (optionnel) + * + * @return status Retourne si oui ou non tout s'est bien passe + * + ---------------------------------------------------------*/ + private function buildRequestObject($uri=null, $params=null, $forced_method=null){ + /* (1) Initialisation + ---------------------------------------------------------*/ + /* (1) Erreur par défaut */ + $this->error = new Error(Err::Success); + + /* (2) Si pas parametre manquant, on quitte */ + if( is_null($uri) ) + return $this->error->set(Err::MissingPath); + + + /* (2) On vérifie la configuration + ---------------------------------------------------------*/ + /* (1) Dispatch if error */ + if( Config::get()->error->get() != Err::Success ) + return ($this->error = Config::get()->error); + + + /* (3) Verification des types des parametres + ---------------------------------------------------------*/ + /* (1) Si path est une */ + if( !is_string($uri) ) // Si le type est incorrect + return $this->error->set(Err::WrongPathModule); + + /* (2) Add slash at the beginning of URI */ + if( !preg_match('@^\/@', $uri) ) + $uri = "/$uri"; + + /* (3) Formattage @params en tableau */ + $this->raw_params = (is_array($params)) ? $params : []; + + /* (4) On définit en constante la méthode HTTP */ + if( !isset($_SERVER['REQUEST_METHOD']) && !is_string($forced_method) ) + return $this->error->set(Err::UnknownHttpMethod); + + $this->http_method = is_string($forced_method) ? strtoupper($forced_method) : strtoupper($_SERVER['REQUEST_METHOD']); + + /* (4) Verification du chemin (existence module+methode) + ---------------------------------------------------------*/ + if( !$this->checkURI($uri) ) // Verification de la coherence du chemin + attribution + return false; // checkURI() sets the error itself + + + /* (5) Si requête de documentation -> on arrête la vérification + ---------------------------------------------------------*/ + if( $this->id['doc_request'] ) + return true; + + + /* (6) Verification des permissions + ---------------------------------------------------------*/ + if( !$this->checkPermission() ) // Si on a pas les droits + return false; // checkPermission() sets the error itself + + + /* (7) Verification des parametres (si @type est defini) + ---------------------------------------------------------*/ + if( !$this->checkParams() ) // Verification de tous les types + return false; // checkParams() sets the error itself + + + /* (8) Récupèration des options + ---------------------------------------------------------*/ + $this->buildOptions(); + + + /* (9) Construction de l'objet + ---------------------------------------------------------*/ + $this->error->set(Err::Success); + + return true; // On retourne que tout s'est bien passe + + } + + + + /* (2) Definit le systeme d'authentification + * + * @instance Instance de type AuthSystem + * + * @return success Whether the AuthSystem is valid or not + * + ---------------------------------------------------------*/ + public static function setAuthSystem($instance=null){ + /* (1) Check instance type */ + if( !($instance instanceof AuthSystem) ) + return false; + + /* (2) Store instance */ + self::$authsystem = $instance; + + return true; + } + + + + /* (3) Verification du format et de la coherence du chemin specifie + * + * @URI URI d'appel (commence par /) + * + * @return validity Retourne si oui ou non l'objet est correct + * + ---------------------------------------------------------*/ + private function checkURI($uri){ + + /* (1) Verification format general + ---------------------------------------------------------*/ + /* (1) If wrong format -> exit */ + if( !preg_match('@^\/[^\/]*(\/[^\/]*)*\/?$@', $uri) ) + return $this->error->set(Err::WrongPathModule); + + /* (2) Add ending '/' if not there */ + if( $uri[strlen($uri)-1] != '/' ) + $uri = "$uri/"; + + + + /* (2) Verification de l'existence du chemin (conf) + ---------------------------------------------------------*/ + /* (1) Check if begins with each indexed @path */ + $exists_size = 0; + $path = null; + + foreach(Config::get()->index as $key=>$void){ + $match_size = strlen($key); + + /* (1.1) Look for the longer ( match | '/' ) */ + if( $match_size > $exists_size && substr($uri, 0, $match_size+1) == "$key/" || $key == '/' ){ + $exists_size = $key == '/' ? 0 : $match_size; + $path = $key; + } + + } + + /* (2) If @path not found -> exit */ + if( is_null($path) ) + return $this->error->set(Err::UnknownModule); + + + /* (3) Extract URI parameters + ---------------------------------------------------------*/ + /* (1) Extract URI string after @path */ + $uri_end = substr($uri, $exists_size); + + /* (2) Special case: add / if root uri arguments */ + if( strlen($uri_end) > 0 && $uri_end[0] != '/' ) + $uri_end = "/$uri_end"; + + /* (3) If invalid format, return error */ + if( !preg_match('@^((?:\/[^\/]*)*)\/?$@', $uri_end, $uri_match) ) + return $this->error->set(Err::InvalidURI); + + /* (4) Add each URI parameter to the parameter store */ + $uri_args = array_slice( explode('/', $uri_match[1]), 1); + + foreach($uri_args as $index=>$value) + if( strlen($value) > 0 ) // do not store '//' empty values + $this->raw_params["URL$index"] = $value; + + + /* (4) Verification de l'existence de la methode (conf) + ---------------------------------------------------------*/ + /* (1) If it is a documentation request */ + $doc_req = $this->http_method == 'OPTIONS'; + + /* (2) Check if HTTP method is in allowed methods */ + if( !in_array($this->http_method, Config::$allowed_http_methods) && !$doc_req ) + return $this->error->set(Err::UnknownHttpMethod, $this->http_method); + + /* (3) Check if HTTP method is defined for this @path */ + if( !isset(Config::get()->index[$path][$this->http_method]) && !$doc_req ) + return $this->error->set(Err::UnknownMethod, $this->http_method); + + + + /* (5) Enregistrement du chemin et renvoi de SUCCESS + ---------------------------------------------------------*/ + $this->id = [ + 'path' => $path, + 'method' => $this->http_method, + 'doc_request' => $doc_req + ]; + + return true; + } + + + + /* (4) Retourne si on a la permission d'executer cette methode + * + * @return permission Retourne si on a les droits ou pas pour executer cette methode + * + ---------------------------------------------------------*/ + private function checkPermission(){ + + /* (1) On recupere les informations utiles + ---------------------------------------------------------*/ + // On recupere le nom de la methode + $method = Config::get()->index[$this->id['path']][$this->id['method']]; + + // Si aucune permission n'est definie + if( !isset($method['per']) || !is_array($method['per']) || count($method['per']) < 1 ) + return true; + + /* (2) Vérification des permissions et de l'authentification + ---------------------------------------------------------*/ + // if no AuthSystem set up, use the default one + if( !is_object(self::$authsystem) || !self::$authsystem instanceof AuthSystem ){ + + // try to load default AuthSystem + if( !file_exists(__BUILD__.'/api/core/AuthSystemDefault.php') ) + return $this->error->set(Err::UnreachableResource); + + // load default AuthSystem class + $classname = '\\api\\core\\AuthSystemDefault'; + self::$authsystem = new $classname(); + } + + // Check permission using user-implemented AuthSystem + $granted = self::$authsystem::permission( $method['per'] ); + + /* (1) On retourne FAUX si aucun droit n'a ete trouve */ + if( $granted->get() !== Err::Success ){ + $this->error = $granted; + return false; + } + + + /* On retourne VRAI si la permission est ok */ + return true; + + } + + + + /* (5) Verification du type des parametres envoyes + * + * @return correct Retourne si oui ou non les parametres ont le bon type + * + ---------------------------------------------------------*/ + private function checkParams(){ + + /* (1) On verifie qu'il ne manque aucun parametre + ---------------------------------------------------------*/ + /* (1) Si @params n'est pas un tableau */ + if( !is_array($this->raw_params) ) + return $this->error->set(Err::MissingParam); + + /* (2) On récupère les données de la méthode */ + $method = Config::get()->index[$this->id['path']][$this->id['method']]; + + /* (3) Si pas 'parameters' dans la config */ + if( !isset($method['par']) || !is_array($method['par']) ) + return $this->error->set(Err::ConfigError); + + + /* (2) Si le type est defini, pour chaque param, on teste + ---------------------------------------------------------*/ + foreach($method['par'] as $name=>$config){ + + /* (2.1) Vérification des données + ---------------------------------------------------------*/ + /* (1) Si @name n'est pas une string */ + if( !is_string($name) ) + return $this->error->set(Err::ConfigError); + + /* (2) Si @config n'est pas un tableau */ + if( !is_array($config) ) + return $this->error->set(Err::ConfigError); + + /* (3) So @config['typ] manquant ou incorrect */ + if( !isset($config['typ']) || !is_string($config['typ']) ) + return $this->error->set(Err::ConfigError); + + + /* (2.2) Gestion des spécifications + ---------------------------------------------------------*/ + /* (1) On récupère le paramètre RENAME */ + $rename = $name; + if( isset($config['ren']) && is_string($config['ren']) && preg_match('@^\w+$@', $config['ren']) ) + $rename = $config['ren']; + + /* (2) On récupère si le paramètre est optionnel ou pas */ + $optional = isset($config['opt']) && $config['opt'] === true; + + /* (3) Gestion du paramètre DEFAULT */ + $default = null; + + /* (3.1) Check if default NOT (NULL || FILE) -> matches TYPE */ + if( isset($config['def']) ){ + + /* (3.1.1) Set default value from config */ + $default = $config['def']; + + /* (3.1.2) If FILE and not null -> Check type */ + if( $config['typ'] != 'FILE' || $default != null ) + if( !Checker::run($config['typ'], $default) ) + return $this->error->set(Err::WrongDefaultParam, $rename, $config['typ']); + + } + + /* (4) Si de type 'FILE' + fichier existe => on enregistre la ref. */ + if( $config['typ'] == 'FILE' && isset($_FILES[$name]) ) + $this->params[$rename] = &$_FILES[$name]; + + /* (4) Si param obligatoire et manquant -> erreur */ + if( !isset($this->raw_params[$name]) && !$optional ) + return $this->error->set(Err::MissingParam, $rename); + + + /* (2.3) Gestion des valeurs + ---------------------------------------------------------*/ + /* (1) Si le paramètre est optionnel et manquant */ + if( $optional && !isset($this->raw_params[$name]) ){ + + // On le crée le param optionnel avec la valeur @default + $this->params[$rename] = $default; + + /* (2) Si le paramètre est renseigné (sauf FILE) */ + }elseif( $config['typ'] != 'FILE' ){ + + // Si la verification est fausse, on retourne faux + if( !Checker::run($config['typ'], $this->raw_params[$name]) ) + return $this->error->set(Err::WrongParam, $rename, $config['typ']); + + // Sinon, on ajoute aux params qu'on enverra à l'appel + else + $this->params[$rename] = $this->raw_params[$name]; + + } + + } + + + /* (3) Gestion du retour, si tout s'est bien passe + ---------------------------------------------------------*/ + return true; + + } + + + + /* (6) Ajout des options a partir de la configuration + * + * @return correct Retourne FAUS en cas d'erreur + * + ---------------------------------------------------------*/ + private function buildOptions(){ + + /* (1) On récupère les options de la méthode en cours + ---------------------------------------------------------*/ + $method = Config::get()->index[$this->id['path']][$this->id['method']]; + + /* (1) Si 'option' n'est pas défini (ou incorrect), on met les valeurs par défaut */ + if( !isset($method['opt']) || !is_array($method['opt']) ) + return true; + + /* (2) Par défaut on définit les options par défaut */ + $this->options = self::$default_options; + + /* (3) On récupère les options données */ + $options = $method['opt']; + + + /* (2) Gestion des différentes options + ---------------------------------------------------------*/ + foreach($options as $option=>$value){ + + /* (1) On ne prend en compte l'option que si elle est dans les options par défaut */ + if( !isset(self::$default_options[$option]) ) + continue; + + /* (2) Le type de la valeur doit être le même que celui de la valeur par défaut */ + if( gettype($value) != gettype(self::$default_options[$option]) ) + continue; + + /* (3) Si tout est bon, on définit la valeur */ + + $this->options[$option] = $value; + } + + return true; + + } + + + + /* (7) Execute le traitement associe et remplie la reponse + * + * @return answer Retourne une reponse de type si tout s'est bien passe + * + ---------------------------------------------------------*/ + public function dispatch(){ + + /* (1) Vérifications de niveau 0 + ---------------------------------------------------------*/ + /* (1) Si erreur -> on dispatch à la réponse */ + if( $this->error->get() !== Err::Success ) + return new Response($this->error); + + /* (2) S'il requête de documentation -> on génère la documentation */ + if( $this->id['doc_request'] ) + return Documentation::generate($this); + + + /* (2) On essaie d'instancier le module + ---------------------------------------------------------*/ + $instance = ModuleFactory::getModule($this->id['path']); + + if( $instance instanceof Error ){ + $this->error->set(Err::UncallableModule, $this->id['path']); + return new Response($this->error); + } + + + /* (3) On verifie que la methode est amorcable + ---------------------------------------------------------*/ + if( !is_callable([$instance, $this->id['method']]) ){ + $this->error->set(Err::UncallableMethod, $this->id['method']); + return new Response($this->error); + } + + /* (4) On amorce la methode + ---------------------------------------------------------*/ + /* (1) On lance la fonction */ + $returned = call_user_func( [$instance, $this->id['method']], $this->params ); + + /* (2) On appelle le destructeur (si défini) */ + $instance = null; + + + /* (5) Gestion de la reponse + ---------------------------------------------------------*/ + /* (1) S'il s'agit d'un téléchargement -> on dispatch */ + if( $this->options['download'] === true ) + return $this->download(); + + /* (2) On construit la réponse avec l'erreur */ + $response = new Response($this->error); + + /* (3) On ajoute les données */ + $response->appendAll($returned); + + /* (4) On retourne la réponse */ + return $response; + + } + + + + /* (8) Gestion d'un téléchargement HTTP + * + */ + public function download(){ + + /* (1) Vérification des erreurs et paramètres + ---------------------------------------------------------*/ + /* (1) Si retourne 'error' et n'est pas SUCCESS -> error */ + if( isset($returned['error']) && $returned['error'] instanceof Error && $returned['error']->get() != Err::Success ){ + $this->error = $returned['error']; + return new Response($this->error); + } + + /* (2) Vérification du contenu, si pas défini */ + if( !isset($returned['body']) ){ + $this->error->set(Err::MissingBody); + return new Response($this->error); + } + + /* (3) Si @headers n'est pas défini on met par défaut */ + if( !isset($returned['headers']) || !is_array($returned['headers']) ){ + $this->error->set(Err::MissingHeaders); + return new Response($this->error); + } + + /* (4) Détermine si téléchargement AJAX/DIRECT */ + $from_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + + + /* (2) Gestion du téléchargement direct (HTTP) + ---------------------------------------------------------*/ + if( !$from_ajax ){ + + /* (1) On définit les headers */ + foreach($returned['headers'] as $header=>$value) + header($header.': '.$value); + + /* (2) On affiche le contenu */ + echo $returned['body']; + + return true; + + } + + + /* (3) Gestion du téléchargement différé (AJAX) + ---------------------------------------------------------*/ + /* (1) On génère les noms de fichiers utiles */ + $target_fname = '/tmp/download_'.uniqid().'.php'; // cible + $buffer_fname = __ROOT__.'/tmp/content_'.uniqid().'.php'; // buffer + + /* (2) On écrit le BODY dans un fichier buffer */ + $buffer_file = fopen($buffer_fname, 'w'); + fwrite($buffer_file, $returned['body']); + fclose($buffer_file); + + /* (3) On crée le fichier cible */ + $target_fnameroot = __PUBLIC__.$target_fname; + $taret_file = fopen($target_fnameroot, 'w'); + fwrite($taret_file, '$value) + fwrite($taret_file, "header(\"$header: $value\");".PHP_EOL); + + /* (5) Script qui écrira le contenu du buffer */ + chmod($buffer_fname, 0775); + fwrite($taret_file, "readfile('$buffer_fname');".PHP_EOL); + + /* (6) Script qui supprimera les fichiers: buffer+target */ + fwrite($taret_file, "unlink('$buffer_fname');".PHP_EOL); + fwrite($taret_file, "unlink(__FILE__);".PHP_EOL); + fwrite($taret_file, '?>'.PHP_EOL); + + /* (7) On ferme le fichier cible */ + fclose($taret_file); + chmod($target_fnameroot, 0775); + + /* (8) On envoie la réponse contenant le lien du fichier cible */ + $response = new Response($this->error); + $response->append('link', $target_fname); + + return $response; + + } + + + + /* (9) Getter générique + * + * @index Index de l'attribut + * + ---------------------------------------------------------*/ + public function get($index=null){ + + switch($index){ + + case 'id': return $this->id; break; + case 'raw_params': return $this->raw_params; break; + case 'params': return $this->params; break; + case 'options': return $this->options; break; + case 'http_method': return $this->http_method; break; + } + + return null; + } + + + } + +?> diff --git a/build/api/core/Response.php b/build/api/core/Response.php new file mode 100755 index 0000000..5cd3e82 --- /dev/null +++ b/build/api/core/Response.php @@ -0,0 +1,133 @@ + Erreur passee par la requete (si existe) + * + */ + public function __construct($error=null){ + if( !( $error instanceof Error ) ) + $error = new Error(Err::Success); + + $this->data = []; + $this->error = $error; + } + /* AJOUTE UNE DONNEE A LA REPONSE + + + * + * @key Le nom de la valeur a ajouter + * @value La valeur a ajouter + * + */ + public function append($key, $value){ + // Ajoute une entree pour la cle @key et de valeur @value + $this->data[$key] = $value; + + return $this; + } + + + /* AJOUTE TOUTES LES DONNEES A LA REPONSE + * + * @dataset Le tableau associatif correspondant a la reponse + * + */ + public function appendAll($dataset){ + // Si ce n'est pas un tableau, on ne fais rien + if( !is_array($dataset) ) + return $this; + + // Si une valeur contient une erreur + if( array_key_exists('error', $dataset) && $dataset['error'] instanceof Error){ + // On definit cette erreur + $this->error = $dataset['error']; + // On enleve cette entree des donnees + unset($dataset['error']); + } + + // Ajoute une entree pour la cle @key et de valeur @value + $this->data = $dataset; + + return $this; + } + /* RECUPERE UNE DONNEE DE LA REPONSE + + + * + * @key Le nom de la valeur a recuperer + * + * @return value La valeur a cette cle + * @return error Retourne NULL si aucune valeur pour cette cle + * + */ + public function get($key){ + // Si la valeur de cle @key n'existe pas, on retourne NULL + if( !isset($this->data[$key]) ) + return null; + + // Sinon, on retourne la valeur associee + return $this->data[$key]; + } + + + /* RECUPERE TOUTES LES DONNEES DE LA REPONSE + * + * @return data Les donnees de la reponse + * + */ + public function getAll(){ + // Sinon, on retourne la valeur associee + return $this->data; + } + + + /* SERIALISATION A PARTIR DES DONNEES + * + * @return json Retourne les donnees serialisees + * + */ + public function serialize(){ + + // Code Http + $this->error->setHttpCode(); + + // Type de contenu + header('Content-Type: application/json; charset=utf-8'); + + // On rajoute l'erreur au message + $returnData = array_merge([ + 'error' => $this->error->get(), + 'ErrorDescription' => $this->error->explicit() + ], + $this->data + ); + + return json_encode($returnData); + + } + + } + +?> diff --git a/build/database/core/DatabaseDriver.php b/build/database/core/DatabaseDriver.php new file mode 100755 index 0000000..83007eb --- /dev/null +++ b/build/database/core/DatabaseDriver.php @@ -0,0 +1,197 @@ + Database Server's host + * @dbname Database name + * @username Database username + * @password Database password + * + */ + private function __construct($host, $dbname, $username, $password){ + /* (2) Stores configuration */ + $this->host = $host; + $this->dbname = $dbname; + $this->username = $username; + $this->password = $password; + + try{ + + $this->pdo = new \PDO('mysql:host='.$this->host.';dbname='.$this->dbname, $this->username, $this->password, [ + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_TIMEOUT => 5 + ]); + + // On signale que tout s'est bien passe + $this->error = new Error(Err::Success); + + }catch(Exception $e){ + // On signale qu'il y a une erreur + $this->error = new Error(Err::PDOConnection); + } + } + + + + /************************************************ + **** Multiton Management (static) **** + ************************************************/ + + /* ADDS A NEW CONNECTION + * + * @label [optional] Database Label + * + * @return status If added successfully + * + */ + private static function add($label=null){ + $conf = self::conf(); + + /* [1] Default values + =========================================================*/ + /* (1) If label isn't given */ + is_null($label) && ($label = 'default'); + + /* (2) If label and no path */ + if( $label !== 'default' && !isset($conf[$label]) ) + return false; + + + /* [3] Instanciates the driver + =========================================================*/ + try{ + + /* (1) If local -> instanciates with local configuration */ + // if( !checkdnsrr($_SERVER['SERVER_NAME'], 'NS') ) + self::$instance[$label] = new DatabaseDriver($conf[$label]['local']['host'], $conf[$label]['local']['dbname'], $conf[$label]['local']['user'], $conf[$label]['local']['password']); + /* (2) If Remote -> instanciates with Remote configuration */ + // else + // self::$instance[$label] = new DatabaseDriver($conf[$label]['remote']['host'], $conf[$label]['remote']['dbname'], $conf[$label]['remote']['user'], $conf[$label]['remote']['password']); + + return true; + + }catch(\Exception $e){ + + /* (3) If fails */ + return false; + + } + + } + + + /* GET A DATABASE DRIVER INSTANCE + * + * @label [optional] Driver's label + * + * @return driver Multiton + * + */ + public static function get($label=null){ + $conf = self::conf(); + + /* [1] Checks arguments + =========================================================*/ + /* (1) Label default value */ + is_null($label) && ($label = 'default'); + + /* (2) If no label, or unknown label */ + if( is_null($label) || !isset(self::$instance[$label]) ){ + + /* (2.1) Try to add the configuration if exists */ + if( isset($conf[$label]) ){ + self::add($label); + return self::get($label); + } + + + throw new \Exception('Database @label is incorrect.'); + } + + + /* [2] Returns instance + =========================================================*/ + return self::$instance[$label]; + } + + + /** retourne la connection statique + * @param null $label + * @return \PDO + */ + public static function getPDO($label=null){ + $instance = self::get($label); + + return $instance->pdo; + } + + + public function pdo(){ + return $this->pdo; + } + + + public function getConfig(){ + return [ + 'host' => $this->host, + 'dbname' => $this->dbname, + 'username' => $this->username + ]; + } + + + + } +?> diff --git a/build/database/core/Repo.php b/build/database/core/Repo.php new file mode 100644 index 0000000..d591b3d --- /dev/null +++ b/build/database/core/Repo.php @@ -0,0 +1,79 @@ +pdo()); + + + /* (3) Check if the method exists */ + if( !\method_exists($instance, $method) ) + throw new \Exception("Repo '$repo' has no public method '$method'"); + + /* (4) Fetch response (send arguments as well) */ + $response = call_user_func_array([$instance, $method], array_slice(func_get_args(), 2)); + + /* (5) Call post-script */ + $instance = null; + + /* (6) Dispatch response */ + return $response; + + } + + + + + } \ No newline at end of file diff --git a/build/database/core/Repo_i.php b/build/database/core/Repo_i.php new file mode 100644 index 0000000..5b9aad5 --- /dev/null +++ b/build/database/core/Repo_i.php @@ -0,0 +1,18 @@ +pdo = $pdo; + + } + + } \ No newline at end of file diff --git a/build/database/repo/user.php b/build/database/repo/user.php new file mode 100644 index 0000000..5223588 --- /dev/null +++ b/build/database/repo/user.php @@ -0,0 +1,250 @@ + The user list + * FALSE on error + * + ---------------------------------------------------------*/ + public function getAll(){ + + /* (1) Statement */ + $st = $this->pdo->query("SELECT * FROM `user` ORDER BY `username` ASC"); + + /* (2) Fetched data */ + return $st->fetchAll(); + + } + + + /* (2) Return a user by its `id_user` + * + * @id_user The user UID + * + * @return user The user if found + * FALSE on error + * + ---------------------------------------------------------*/ + public function getById(int $id_user){ + + /* (1) Prepare Statement */ + $pst = $this->pdo->prepare("SELECT * FROM `user` WHERE `id_user` = :id_user LIMIT 1"); + + /* (2) Bind variables */ + $pst->bindParam(':id_user', $id_user, \PDO::PARAM_INT); + + /* (3) Execute */ + if( !$pst->execute() ) return false; // if error -> send FALSE + + /* (4) Fetched data */ + return $pst->fetch(); + + } + + + /* (3) Return a user by its `mail` + * + * @mail The user mail address + * + * @return user The user if found + * FALSE on error + * + ---------------------------------------------------------*/ + public function getByMail(String $mail){ + + /* (1) Prepare Statement */ + $pst = $this->pdo->prepare("SELECT * FROM `user` WHERE `mail` = :mail LIMIT 1"); + + /* (2) Bind variables */ + $pst->bindParam(':mail', $mail, \PDO::PARAM_STR, 50); + + /* (3) Execute */ + if( !$pst->execute() ) return false; // if error -> send FALSE + + /* (4) Fetched data */ + return $pst->fetch(); + + } + + + /* (4) Return a user by its `username` + * + * @username The user username + * + * @return user The user if found + * FALSE on error + * + ---------------------------------------------------------*/ + public function getByUsername(String $username){ + + /* (1) Prepare Statement */ + $pst = $this->pdo->prepare("SELECT * FROM `user` WHERE `username` = :username LIMIT 1"); + + /* (2) Bind variables */ + $pst->bindParam(':username', $username, \PDO::PARAM_STR, 20); + + /* (3) Execute */ + if( !$pst->execute() ) return false; // if error -> send FALSE + + /* (4) Fetched data */ + return $pst->fetch(); + + } + + + /* (5) Return a user by its `token` + * + * @token The user token + * + * @return user The user if found + * FALSE on error + * + ---------------------------------------------------------*/ + public function getByToken(String $token){ + + /* (1) Prepare Statement */ + $pst = $this->pdo->prepare("SELECT * FROM `user` WHERE `token` is not NULL AND `token` = :token LIMIT 1"); + + /* (2) Bind variables */ + $pst->bindParam(':token', $token, \PDO::PARAM_STR, 128); + + /* (3) Execute */ + if( !$pst->execute() ) return false; // if error -> send FALSE + + /* (4) Fetched data */ + return $pst->fetch(); + + } + + + /* (6) Check the password of a user + * + * @id_user The user UID + * @password The password to test + * + * @return valid Whether the password is valid or not + * + ---------------------------------------------------------*/ + public function checkPassword(int $id_user, String $password){ + + /* (1) Hash the password */ + $hash = \secure_hash($password, $id_user, 'user-pass'); + + /* (2) Prepare Statement */ + $pst = $this->pdo->prepare("SELECT * FROM `user` WHERE `id_user` = :id_user AND `pass` = :pass LIMIT 1"); + + /* (3) Bind variables */ + $pst->bindParam(':id_user', $id_user, \PDO::PARAM_INT); + $pst->bindParam(':pass', $hash, \PDO::PARAM_STR, 128); + + /* (4) Execute */ + if( !$pst->execute() ) return false; // if error -> send FALSE + + /* (5) If no data -> means invalid password */ + if( !is_array($pst->fetch()) ) + return false; + + /* (6) If here -> means password is ok */ + return true; + + } + + + /* (6) Set the password for a user + * + * @id_user The user UID + * @password The password to set + * + * @return set Whether the password has been set or not + * + ---------------------------------------------------------*/ + public function setPassword(int $id_user, String $password){ + + /* (1) Hash the password */ + $hash = \secure_hash($password, $id_user, 'user-pass'); + + /* (2) Prepare Statement */ + $pst = $this->pdo->prepare("UPDATE `user` SET `pass` = :pass WHERE `id_user` = :id_user"); + + /* (3) Bind variables */ + $pst->bindParam(':pass', $hash, \PDO::PARAM_STR, 128); + $pst->bindParam(':id_user', $id_user, \PDO::PARAM_INT); + + /* (4) Execute -> dispatch status */ + return $pst->execute(); + + } + + /* (7) Creates a new user + * + * @username The username (must be unique) + * @mail The mail address (must be unique) + * @password The password + * + * @return id_created UID of the created user + * FALSE on error + * + ---------------------------------------------------------*/ + public function create(String $username, String $mail, String $password){ + + + /* (1) Check @username + @mail are unique + ---------------------------------------------------------*/ + /* (1) If @username already exists -> abort */ + if( is_array($this->getByUsername($username)) ) + return false; + + /* (2) If @mail already exists -> abort */ + if( is_array($this->getByMail($mail)) ) + return false; + + + + /* (2) Create the user (without password) + ---------------------------------------------------------*/ + /* (1) Create a random token */ + $token = \secure_hash(uniqid(), 'user-token'); + + /* (2) Prepare Statement */ + $pst = $this->pdo->prepare("INSERT INTO `user`(`id_user`, `username`, `mail`, `pass`, `token`) VALUES(DEFAULT, :username, :mail, NULL, :token)"); + + /* (3) Bind variables */ + $pst->bindParam(':username', $username, \PDO::PARAM_STR, 20); + $pst->bindParam(':mail', $mail, \PDO::PARAM_STR, 50); + $pst->bindParam(':token', $token, \PDO::PARAM_STR, 128); + + /* (4) Execute -> if error return FALSE */ + if( !$pst->execute() ) return false; + + + /* (2) Set the password (needed @id_user) + ---------------------------------------------------------*/ + /* (1) Get last inserted id */ + $fetch_user = $this->getByUsername($username); + + /* (2) If nothing found -> error */ + if( !is_array($fetch_user) || !isset($fetch_user['id_user']) || !is_numeric($fetch_user['id_user']) ) + return false; + + /* (3) Extract @id_user */ + $id_user = intval($fetch_user['id_user']); + + /* (4) Repo self call */ + if( !$this->setPassword($id_user, $password) ) + return false; + + /* (5) Return @id_user */ + return $id_user; + + } + + + + + } \ No newline at end of file diff --git a/build/router/controller/api.php b/build/router/controller/api.php new file mode 100644 index 0000000..6778a66 --- /dev/null +++ b/build/router/controller/api.php @@ -0,0 +1,56 @@ + Calling URI + * + */ + public function __construct($matches){ + + /* (1) Rebuild request url */ + $uri = $matches['uri']; + + /* (2) Creates request */ + $this->request = Loader::remote($uri); + + } + + + /* CALL + * + */ + public function call(){ + + /* (1) Process response */ + $this->response = $this->request->dispatch(); + + /* (2) Manages result */ + if( $this->response instanceof Response ) + echo $this->response->serialize(); + + return true; + + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/controller/page.php b/build/router/controller/page.php new file mode 100644 index 0000000..9a132d5 --- /dev/null +++ b/build/router/controller/page.php @@ -0,0 +1,41 @@ + Calling URI + * + */ + public function __construct($url){ + $this->pagename = $url['page']; + + } + + + /* CALL + * + */ + public function load(){ + if( file_exists(__PUBLIC__."/view/".$this->pagename.".php") ) + include __PUBLIC__."/view/".$this->pagename.".php"; + else + echo "page not found"; + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/controller/redirect.php b/build/router/controller/redirect.php new file mode 100644 index 0000000..7b10307 --- /dev/null +++ b/build/router/controller/redirect.php @@ -0,0 +1,34 @@ + Calling URI + * + */ + public function __construct($url){ + } + + + /* CALL + * + */ + public function homepage(){ + header('Location: /homepage/'); + } + + /* POST-CALL + * + */ + public function __destruct(){ + + } + + + + } diff --git a/build/router/core/ControllerFactory.php b/build/router/core/ControllerFactory.php new file mode 100644 index 0000000..ad2d642 --- /dev/null +++ b/build/router/core/ControllerFactory.php @@ -0,0 +1,51 @@ + Nom du controller + * + * @return exists Si oui ou non le controller existe + * + */ + public static function checkController($controller){ + /* (1) Check type + pattern */ + if( !is_string($controller) || !preg_match('/^[A-Za-z_]\w+$/', $controller) ) + return false; + + /* (2) On vérifie que la classe existe */ + if( !file_exists(__BUILD__."/router/controller/$controller.php") ) + return false; + + /* (3) Sinon il existe */ + return true; + } + + + + /* INSTANCIE UN CONTROLLER + * + * @controller Nom du controller + * @arguments [OPTIONNEL] Arguments à passer au constructeur + * + * @return instance Instance du controller en question + * + */ + public static function getController($controller, $arguments=[]){ + /* (1) On vérifie l'existance du controller */ + if( !self::checkController($controller) ) + return false; + + /* (2) On récupère la classe */ + $class_name = "\\router\\controller\\$controller"; + + /* (3) On retourne une instance */ + return new $class_name($arguments); + } + + } diff --git a/build/router/core/Route.php b/build/router/core/Route.php new file mode 100644 index 0000000..83bfd95 --- /dev/null +++ b/build/router/core/Route.php @@ -0,0 +1,100 @@ + Pattern correspondant a la route + * @controller Controller de la route + * @method Methode du controller + * + * @return instance Retour de l'instance courante + * + =========================================================*/ + public function __construct($pattern=null, $controller=null, $method=null){ + // Note: all arguments must be verified by 'Router->add' method + + /* (1) Pattern -> regex format */ + $this->pattern = "/^$pattern$/"; + + /* (2) Controller */ + $this->controller = $controller; + + /* (3) Controller's method */ + $this->method = $method; + + /* (4) Initialize matches */ + $this->matches = []; + + } + + + + /* [3] Checks if route matches URL + * + * @url URL + * + * @return matches If matches URL + * + =========================================================*/ + public function match($url){ + + /* (1) If doesn't matches @url -> false */ + if( !preg_match($this->pattern, $url, $matches) ) + return false; + + /* (2) Return only named matches */ + foreach($matches as $name=>$match) + if( !is_numeric($name) ) + $this->matches[$name] = $match; + + /* (4) Add complete URL */ + $this->matches['__URL__'] = $url; + + /* (5) Return status */ + return true; + } + + + /* [4] Method call + * + * @return response Response + * + =========================================================*/ + public function call(){ + /* (1) Instanciate controller */ + $instance = ControllerFactory::getController($this->controller, $this->matches); + + /* (2) Launch method & catch response */ + $response = call_user_func([$instance, $this->method]); + + /* (3) Call controller's destructor */ + $instance = null; + + /* (4) Return response */ + return $response; + } + + } diff --git a/build/router/core/Router.php b/build/router/core/Router.php new file mode 100644 index 0000000..f2200a7 --- /dev/null +++ b/build/router/core/Router.php @@ -0,0 +1,333 @@ +run(); + + } + + + /* [3] Constructor + * + * @url Current URL + * + * @return instance Instance du routeur + * + =========================================================*/ + public function __construct($url=null){ + /* (1) Checks arguments + ---------------------------------------------------------*/ + /* (1) Default value if incorrect */ + $this->url = is_string($url) ? $url : ''; + + /* (2) Add first '/' if missing */ + if( !preg_match('/^\//', $url) ) + $this->url = '/'.$this->url; + + + /* (2) Loads configuration + ---------------------------------------------------------*/ + /* (1) Tries to load configuration */ + $this->cnf = self::loadConfig(); + + /* (2) If error occurs, throw Exception */ + if( is_null($this->cnf) ) + throw new \Exception("[Router] Configuration file error found"); + + + /* (3) Set allowed HTTP methods + ---------------------------------------------------------*/ + /* (1) If not defined */ + if( !isset($this->cnf['methods']) ) + throw new \Exception('[Router] Configuration file error, \'methods\' clause missing'); + + /* (2) Try to clean methods */ + $this->http_methods = self::cleanMethods($this->cnf['methods']); + + /* (3) Manage error */ + if( is_null($this->http_methods) ) + throw new \Exception('[Router] Configuration file error. \'methods\' must be an array of HTTP methods ["GET", "POST", ...]'); + + + /* (4) Initialize routes + ---------------------------------------------------------*/ + /* (1) Init routes */ + $this->routes = []; + + foreach($this->http_methods as $method) + $this->routes[$method] = []; + + /* (2) Default configuration if missing */ + if( !isset($this->cnf['routes']) || !is_array($this->cnf['routes']) ) + $this->cnf['routes'] = []; + + + /* (5) Loads each route + ---------------------------------------------------------*/ + foreach($this->cnf['routes'] as $pattern=>$route){ + + /* (1) If missing (required) parameters */ + if( !isset($route['controller']) ) + continue; + + /* (2) Default value for 'methods' */ + ( !isset($route['methods']) || !is_array($route['methods']) ) && ($route['methods'] = $this->http_methods); + + /* (3) Default value for 'arguments' */ + ( !isset($route['arguments']) || !is_array($route['arguments']) ) && ($route['arguments'] = []); + + /* (4) Add route */ + $added = $this->add($pattern, $route['controller'], $route['arguments']); + + // if error -> next + if( $added === false ) + continue; + + + /* (5) Add route for each method */ + foreach($route['methods'] as $method) + if( in_array($method, $this->http_methods) ) + $this->routes[$method][] = $added; + + } + + + } + + + + + /* [4] Adds a route + * + * @pattern URL pattern with {somevar} variables within + * @controller Controller name + method "controllername:methodname" + * @arguments List of pattern's arguments and their RegExp composition (default is alphanumeric) + * + * @return route New instance of Route || false on error + * + =========================================================*/ + public function add($pattern=null, $controller=null, $arguments=[]){ + + /* (1) Format and check pattern + ---------------------------------------------------------*/ + /* (1) If not a string */ + if( !is_string($pattern) ) + return false; + + /* (2) Format pattern and check result */ + $pattern = self::formatPattern($pattern, $arguments); + + if( $pattern === false ) + return false; + + + /* (2) Check controller + ---------------------------------------------------------*/ + /* (1) Check default type */ + if( !is_string($controller) || !preg_match('/^([A-Za-z_]\w+):([A-Za-z_]\w+)$/', $controller, $c_matches) ) + return false; + + /* (2) Check existence */ + if( !ControllerFactory::checkController($c_matches[1]) ) + return false; + + + /* (3) Check method + ---------------------------------------------------------*/ + if( !method_exists('\\router\\controller\\'.$c_matches[1], $c_matches[2]) ) + return false; + + + /* (4) Return new route + ---------------------------------------------------------*/ + return new Route($pattern, $c_matches[1], $c_matches[2]); + } + + + + + /* [5] Router launch + * + =========================================================*/ + public function run(){ + /* (1) Manage HTTP method + ---------------------------------------------------------*/ + /* (1) Fetch HTTP method */ + $httpMethod = $_SERVER['REQUEST_METHOD']; + + /* (2) If no route for this -> exit */ + if( !isset($this->routes[$httpMethod]) || count($this->routes[$httpMethod]) <= 0 ) + return false; + + + /* (2) Manage routes (matching) + ---------------------------------------------------------*/ + /* (1) Check for each HTTP method's route */ + foreach($this->routes[$httpMethod] as $route) + + /* (2) First route that matches -> call & return response */ + if( $route->match($this->url) ) + return $route->call(); + + + /* (3) If no route found -> return false + ---------------------------------------------------------*/ + return false; + } + + + /* FORMATS A PATTERN + * + * @pattern Pattern to process on + * @arguments List of used arguments, with regex if given + * @vars [OPT] If variable replacement have to be done + * + * @return formatted Formatted pattern || false on error + * + */ + public static function formatPattern($pattern, $arguments=[]){ + + /* (1) Check arguments + ---------------------------------------------------------*/ + /* (1) Check minimal length */ + if( strlen($pattern) < 1 ) + return false; + + /* (2) Arguments formatting */ + $arguments = !is_array($arguments) ? [] : $arguments; + + + /* (2) Replace special characters + replace vars + ---------------------------------------------------------*/ + /* (1) Check default URL format */ + if( !preg_match('/^(\/[\w\{\}-]*)*\/?$/', $pattern) ) + return false; + + /* (2) Escape special characters */ + $pattern = str_replace('/', '\\/', $pattern); + + /* (3) Add optional ending '/' */ + if( !preg_match('/\/$/', $pattern) ) + $pattern .= '\\/?'; + + /* (4) Replace variable by tagged capturing groups */ + $boundary = self::randBoundary(); + $pattern = preg_replace('/\{([a-z_][a-z0-9_]*)\}/i', '(?P<$1>'.$boundary.'-$1-'.$boundary.')', $pattern); + + + /* (3) Variable replacement + ---------------------------------------------------------*/ + /* (1) List variables */ + $vars = []; + $var_pattern = '/'.$boundary.'\-([A-Za-z_][A-Za-z0-9_]*)\-'.$boundary.'/'; + preg_match_all($var_pattern, $pattern, $matches); + + /* (2) For each matching variable -> replace with associated regex */ + if( is_array($matches) && isset($matches[1]) ){ + + foreach($matches[1] as $m=>$varname){ + + // {3.1.1} Not in @arguments -> default regex // + if( !isset($arguments[$varname]) || !is_string($arguments[$varname]) ){ + $pattern = str_replace($matches[0][$m], '[A-Za-z0-9_]+', $pattern); + continue; + } + + // {3.1.2} If variable in @arguments -> set regex without capturing-groups // + // $without_capg = str_replace('(', '(?:', $arguments[$varname]); + $pattern = str_replace($matches[0][$m], $arguments[$varname], $pattern); + + } + + } + + + /* (4) Return formatted pattern + ---------------------------------------------------------*/ + return $pattern; + } + + + + /* LOADS CONFIGURATION + * + * @return cnf Configuration content || NULL if error + * + */ + private static function loadConfig(){ + /* (1) Set configuration file's path */ + $cnfpath = self::config_path(); + + /* (2) Checks file */ + if( !file_exists($cnfpath) ) + return null; // throw new \Exception("[Router] Configuration file not found"); + + /* (3) Checks format -> null if error */ + return json_decode( file_get_contents($cnfpath), true ); + + } + + + /* CHECKS METHODS AND CLEAN THE LIST IF CORRECT + * + * @wanted List of wanted methods + * + * @return cleaned Cleaned methods || null if error + * + */ + private static function cleanMethods($wanted=[]){ + /* (1) Checks @wanted */ + if( !is_array($wanted) || count($wanted) < 1 ) + return null; // throw new \Exception('[Router] Configuration file error, \'methods\' must be an array containing managed HTTP methods'); + + /* (2) Set methods (uppercase and unique) */ + $cleaned = []; + + foreach($wanted as $method) + if( !in_array(strtoupper($method), $cleaned) ) + $cleaned[] = strtoupper($method); + + /* (3) Return cleaned method list */ + return $cleaned; + } + + } diff --git a/config/database-driver.json b/config/database-driver.json new file mode 100755 index 0000000..fb2da3c --- /dev/null +++ b/config/database-driver.json @@ -0,0 +1,16 @@ +{ + "default": { + "local": { + "host" : "mariadb", + "dbname" : "vhost", + "user" : "php", + "password" : "4JB1dtbrIC8pT935" + }, + "remote": { + "host" : "db_remote_host", + "dbname" : "db_remote_name", + "user" : "db_remote_user", + "password" : "db_remote_password" + } + } +} diff --git a/config/modules.json b/config/modules.json new file mode 100644 index 0000000..c79936b --- /dev/null +++ b/config/modules.json @@ -0,0 +1,94 @@ +{ + + "article": { + + "POST": { + "description": "Posts a new article", + "permissions": ["journalist"], + "parameters": { + "title": { "description": "Article's title", "type": "varchar(5,100)" }, + "content": { "description": "Article's content", "type": "text" } + }, + "output": { + "created_id": { "description": "Id of the created article", "type": "id" } + } + }, + + "GET": { + "description": "Gets all or a specific article", + "permissions": ["viewer", "journalist"], + "parameters": { + "URL0": { "description": "Article id", "type": "id", "optional": true, "rename": "id_article" } + }, + "output": { + "articles": { "description": "List of selected articles", "type": "array" } + } + }, + + "VIEW": { + "description": "Gets a specific article into a json file (download)", + "permissions": ["viewer", "journalist"], + "options": { "download": true }, + "parameters": { + "URL0": { "description": "Article id", "type": "id", "rename": "id_article" } + }, + "output": { + "article": { "description": "Selected article as JSON file", "type": "text" } + } + }, + + "PUT": { + "description": "Updates a specific article", + "permissions": ["journalist"], + "parameters": { + "URL0": { "description": "Article id", "type": "id", "rename": "id_article" }, + "content": { "description": "Article's content", "type": "text" } + }, + "output": { + "article": { "description": "Returns updated article", "type": "array" } + } + }, + + "DELETE": { + "description": "Deletes a specific article", + "permissions": ["journalist"], + "parameters": { + "URL0": { "description": "Article id", "type": "id", "rename": "id_article" } + }, + "output": {} + } + + }, + + + "encapsuled": { + "url": { + "with": { + "possible": { + "methods": { + "at": { + + "each": { + "level": { + + "GET": { + "description": "GET /encapsuled/url/with/possible/methods/at/each/level", + "permissions": [], + "parameters": {} + } + + } + }, + + "POST": { + "description": "POST /encapsuled/url/with/possible/methods/at", + "permissions": [], + "parameters": {} + } + } + } + } + } + } + } +} diff --git a/config/routes.json b/config/routes.json new file mode 100644 index 0000000..8019426 --- /dev/null +++ b/config/routes.json @@ -0,0 +1,34 @@ +{ + + "methods": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ], + + + "routes": { + + "/{page}/": { + "methods": ["GET"], + "controller": "page:load", + "arguments": { + "page": "[a-z]+" + } + }, + + "/api/v/1.0/{uri}": { + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "controller": "api:call", + "arguments": { + "uri": ".*" + } + }, + + "/{any}": { + "methods": ["GET"], + "controller": "redirect:homepage", + "arguments": { + "any": ".*" + } + } + + } + +} diff --git a/public_html/index.php b/public_html/index.php new file mode 100644 index 0000000..c57fb65 --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,20 @@ +