From 8371ea89b816229d4a326154ba82d96f230d3536 Mon Sep 17 00:00:00 2001 From: xdrm-brackets Date: Thu, 23 Nov 2017 11:23:09 +0100 Subject: [PATCH] Barebone setup@0 > build: api, router, http, error | --- .htaccess | 3 + README.md | 12 +- autoloader.php | 46 ++ build/.htaccess | 2 + build/api/core/AuthSystem.php | 29 ++ build/api/core/AuthSystemDefault.php | 273 ++++++++++++ build/api/core/Checker.php | 155 +++++++ build/api/core/Loader.php | 106 +++++ build/api/core/ModuleFactory.php | 41 ++ build/api/core/Request.php | 570 ++++++++++++++++++++++++ build/api/core/Response.php | 132 ++++++ build/api/module/RESTexample.php | 130 ++++++ build/database/core/DatabaseDriver.php | 190 ++++++++ build/error/core/Err.php | 112 +++++ build/error/core/Error.php | 191 ++++++++ build/http/core/HttpRequest.php | 208 +++++++++ build/router/controller/api.php | 56 +++ build/router/controller/page.php | 41 ++ build/router/controller/redirect.php | 34 ++ build/router/core/ControllerFactory.php | 61 +++ build/router/core/Route.php | 100 +++++ build/router/core/Router.php | 335 ++++++++++++++ config/.htaccess | 2 + config/database-driver.json | 16 + config/modules.json | 60 +++ config/routes.json | 36 ++ public_html/.htaccess | 4 + public_html/index.php | 6 + 28 files changed, 2949 insertions(+), 2 deletions(-) create mode 100755 .htaccess mode change 100644 => 100755 README.md create mode 100755 autoloader.php create mode 100755 build/.htaccess create mode 100755 build/api/core/AuthSystem.php create mode 100755 build/api/core/AuthSystemDefault.php create mode 100755 build/api/core/Checker.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/api/module/RESTexample.php create mode 100755 build/database/core/DatabaseDriver.php create mode 100755 build/error/core/Err.php create mode 100755 build/error/core/Error.php create mode 100755 build/http/core/HttpRequest.php create mode 100755 build/router/controller/api.php create mode 100755 build/router/controller/page.php create mode 100755 build/router/controller/redirect.php create mode 100755 build/router/core/ControllerFactory.php create mode 100755 build/router/core/Route.php create mode 100755 build/router/core/Router.php create mode 100755 config/.htaccess create mode 100755 config/database-driver.json create mode 100755 config/modules.json create mode 100755 config/routes.json create mode 100755 public_html/.htaccess create mode 100755 public_html/index.php diff --git a/.htaccess b/.htaccess new file mode 100755 index 0000000..a80b524 --- /dev/null +++ b/.htaccess @@ -0,0 +1,3 @@ +RewriteEngine on + +RewriteRule ^(.*)$ public_html/$1 [QSA,L] diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 5226ee6..574001d --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ -# ndli1718 +# Nuit de l'Info 2017-18 -Nuit de l'info 2017-2018 \ No newline at end of file +```yaml + event: Nuit de l'Info + year: [2017, 2018] + team: Into The While + members: + - Lucas Mascaro + - Guillaume Fauvet + - Adrien Marquès +``` \ No newline at end of file diff --git a/autoloader.php b/autoloader.php new file mode 100755 index 0000000..b17cf87 --- /dev/null +++ b/autoloader.php @@ -0,0 +1,46 @@ + 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/.htaccess b/build/.htaccess new file mode 100755 index 0000000..93169e4 --- /dev/null +++ b/build/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all 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/AuthSystemDefault.php b/build/api/core/AuthSystemDefault.php new file mode 100755 index 0000000..b74054a --- /dev/null +++ b/build/api/core/AuthSystemDefault.php @@ -0,0 +1,273 @@ + $match[1], 'type' => 'user' ]; + + /* (2) Token Authentication: USER */ + elseif( preg_match('/^(a[a-f0-9]{128})$/', $AUTH, $match) ) + $_SESSION['AUTH'] = [ 'token' => $match[1], 'type' => 'admin' ]; + + /* (2) Aucune authentification */ + else{ + $_SESSION['AUTH'] = []; + $_SESSION['USER'] = []; + $_SESSION['ADMIN'] = []; + } + + /* (4) On vérifie l'authentification par BDD + ---------------------------------------------------------*/ + if( !self::deepCheck() ){ + $_SESSION['AUTH'] = []; + $_SESSION['USER'] = []; + $_SESSION['ADMIN'] = []; + } + } + + + + + /* VERIFICATION DE L'AUTHENTIFICATION + * + * + */ + private static function deepCheck(){ + + /* [1] Si aucune authentification + =========================================================*/ + if( self::auth_level() == 0 ) + return false; + + + /* [2] Si authentification token -> ADMIN + =========================================================*/ + if( self::auth_level() >= 1 ){ + + $checkRoot = new Repo('warehouse/getByToken', [ $_SESSION['AUTH'][0] ]); + + /* (1) Si le token n'existe pas, on retourne une erreur */ + if( $checkRoot->answer() == false ) + return false; + + /* (2) On met à jour les informations de l'entrepot */ + $_SESSION['WAREHOUSE'] = [ + 'id' => (int) $checkRoot->answer()[0]['id_warehouse'], + 'name' => $checkRoot->answer()[0]['name'], + 'theme' => '#'.$checkRoot->answer()[0]['theme'] + ]; + + /* (3) On récupère les modules de l'entrepot */ + $getModules = new Repo('warehouse/getModules', [ $_SESSION['WAREHOUSE']['id'] ]); + + $_SESSION['WAREHOUSE']['modules'] = $getModules->answer(); + + } + + + /* [3] Si authentification token -> USER + =========================================================*/ + if( self::auth_level() == 2 ){ + + $checkBranch = new Repo('admin/getByToken', [ $_SESSION['WAREHOUSE']['id'], $_SESSION['AUTH'][1] ]); + + /* (1) Si le token n'existe pas, on retourne une erreur */ + if( $checkBranch->answer() == false ) + return false; + + /* (2) On met à jour les informations de l'administrateur */ + $_SESSION['ADMIN'] = [ + 'id' => (int) $checkBranch->answer()['id_admin'], + 'username' => $checkBranch->answer()['username'], + 'mail' => $checkBranch->answer()['mail'] + ]; + + + } + + + /* [4] Si authentification triple -> WAREHOUSE + SATS_token + SATS_nexttoken + =========================================================*/ + if( self::auth_level() == 3 ){ + + + $checkBranch = new Repo('machine/checkToken', [ $_SESSION['WAREHOUSE']['id'], $_SESSION['AUTH'][1], $_SESSION['AUTH'][2] ]); + + /* (1) Si le token n'est pas valide, on retourne une erreur */ + if( $checkBranch->answer() === false ) + return false; + + /* (2) On met à jour les informations de l'administrateur */ + $_SESSION['SATS'] = [ + 'id' => (int) $checkBranch->answer() + ]; + + + } + + + /* [5] Si pas d'erreur d'authentification, on retourne TRUE + =========================================================*/ + return true; + } + + + + + + + + /* VERIFICATION DES ACCES EN FONCTION DE PERMISSIONS ATTENDUES + * + * @module Module concerné + * @expected Liste de listes de combinaisons de permissions attendues + * + * @return error Si FALSE, pas la permission, sinon si + * + */ + public static function permission($module, $expected){ + + $error_propag = []; + + /* [1] Check format -> if not array of array(s) -> ERROR + =========================================================*/ + /* (1) If not array -> ERROR */ + if( !is_array($expected) ) + return new Error(Err::FormatError); + + /* (2) If not array of array(s) -> ERROR */ + foreach($expected as $permissions) + if( !is_array($permissions) ) + return new Error(Err::FormatError); + + + /* [2] Foreach each set of permission + =========================================================*/ + foreach($expected as $permission_group){ + + /* If granted -> don't go further */ + $error_propag[]= self::check_permission_group($module, $permission_group); + + if( $error_propag[count($error_propag)-1] == Err::Success ) + return new Error(Err::Success); + + } + + + /* [3] By default return `PermissionError` + =========================================================*/ + if( count($error_propag) > 0 ) + return new Error($error_propag[count($error_propag)-1]); + + return new Error(Err::PermissionError); + } + + + + + + + + /* VERIFICATION DES ACCES EN FONCTION DE PERMISSIONS ATTENDUES + * + * @module Module concerné + * @expected Liste des permissions attendues + * + * @return error Err:: error constants + * + */ + private static function check_permission_group($module, $expected){ + + + /* [1] Gestion de l'AUTH (authentification) + =========================================================*/ + + /* (1) Si entrepot requis, mais manquant + ---------------------------------------------------------*/ + if( in_array('admin', $expected) && ( self::auth_level() < 2 || !isset($_SESSION['ADMIN']['id']) ) ) + return Err::PermissionError; + + /* (2) Si admin requis, mais manquant + ---------------------------------------------------------*/ + if( in_array('user', $expected) && ( self::auth_level() < 1 || !isset($_SESSION['USER']['id']) ) ) + return Err::PermissionError; + + + /* [2] Gestion des permissions + =========================================================*/ + + /* (1) Vérification de toutes les permissions requises */ + foreach($expected as $permission) + // Si il manque au minimum une permission, on retourne FALSE + if( !in_array($permission, $_SESSION['PERM']) ) + return Err::PermissionError; + + + /* [4] Si on a toutes les permissions requises + =========================================================*/ + return Err::Success; + } + + + + + + /* RENVOIE LE NIVEAU D'AUTHENTIFICATION + * + * @return auth Niveau d'authentification (0 à 2) + * + */ + public static function auth_level(){ + + /* (1) Not set */ + if( !is_array($_SESSION['AUTH']) || !isset($_SESSION['AUTH']['type']) ) + return 0; + + /* (2) Admin / User */ + return ($_SESSION['AUTH']['type'] == 'admin') ? 2 : 1; + + } + + } + +?> diff --git a/build/api/core/Checker.php b/build/api/core/Checker.php new file mode 100755 index 0000000..0e88b0f --- /dev/null +++ b/build/api/core/Checker.php @@ -0,0 +1,155 @@ + 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]+$/i', $value) && (strlen($value) == 40 || strlen($value) == 64); + 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; + + default: + return false; + break; + } + + return $checker; + + } + + + } +?> diff --git a/build/api/core/Loader.php b/build/api/core/Loader.php new file mode 100755 index 0000000..0e81010 --- /dev/null +++ b/build/api/core/Loader.php @@ -0,0 +1,106 @@ + 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) Check if @path var is set + ---------------------------------------------------------*/ + /* (1) If is in @uri */ + $pathInUrl = is_string($uri) && preg_match('#^/?([\w_-]+/[\w_-]+)(?:/?|/((?:\w+/)*(?:\w+/?)))$#', $uri, $uriMatches); + + /* (2) Get @path from @uri + @uri arguments if there is */ + if( $pathInUrl ){ + + // {1} Add @path as data // + $data['path'] = $uriMatches[1]; + + // {2} Add $uri arguments as data 'URL_@i' (@i is the order beginnint at 0) // + if( count($uriMatches) > 2 ){ + + $uriParams = explode('/', trim($uriMatches[2], '/')); + + foreach($uriParams as $k=>$v) + $data["URL_$k"] = $v; + + } + + } + + /* (3) If @path haven't been found -> error */ + if( !isset($data['path']) ) + return new Request(); + + + + /* (3) Parse arguments from JSON + ---------------------------------------------------------*/ + /* (1) Init. arguments */ + $params = []; + + /* (2) Parse each arg (except @path) */ + foreach($data as $name=>$value){ + + if( $name === 'path' ) + continue; + + // {1} Json parse // + $json = json_decode( $value, true ); + + // {2} if valid -> set the parsed value // + if( !is_null($json) ) + $params[$name] = $json; + + // {3} else -> leave it like it was // + else + $params[$name] = $value; + } + + + /* (4) Build an API Request object + ---------------------------------------------------------*/ + return new Request($data['path'], $params); + } + + + + + } \ 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..b0d4f6c --- /dev/null +++ b/build/api/core/ModuleFactory.php @@ -0,0 +1,41 @@ + 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 : []; + + /* (1) On vérifie que la classe existe */ + if( !file_exists(__BUILD__."/api/module/$module.php") ) + return false; + + /* (2) On récupère la classe */ + $class_name = "\\api\\module\\$module"; + + /* (3) 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..5a44de6 --- /dev/null +++ b/build/api/core/Request.php @@ -0,0 +1,570 @@ + false + ]; + private static $authsystem = null; + + // Attributs prives utiles (initialisation) + private $path; + private $params; + private $modules; + private $options; + private $http_method; + + // Contiendra la reponse a la requete + public $answer; + + // Contiendra l'etat de la requete + public $error; + + + + + + /* CONSTRUCTEUR D'UNE REQUETE DE MODULE + * + * @path Chemin de delegation ("module/methode") + * @param Tableau associatif contenant les parametres utiles au traitement + * + * @return instance Instance crée + * + */ + public function __construct($path=null, $params=null){ + + return $this->buildRequestObject($path, $params); + + } + + + + /* CONSTRUCTEUR D'UNE REQUETE DE MODULE (DELEGATION) + * + * @path Chemin de delegation ("module/methode") + * @param Tableau associatif contenant les parametres utiles au traitement + * + * @return status Retourne si oui ou non tout s'est bien passe + * + */ + private function buildRequestObject($path=null, $params=null){ + /* [1] Initialisation + =========================================================*/ + /* (1) Erreur par défaut */ + $this->error = new Error(Err::Success); + + /* (2) Si pas parametre manquant, on quitte */ + if( $path == null ) + return $this->error->set(Err::MissingPath); + + + /* [2] On met a jour la configuration + =========================================================*/ + /* (1) Vérification existence fichier config */ + if( !file_exists(self::config_path()) ) + return $this->error->set(Err::UnreachableResource); + + /* (2) Lecture fichier config */ + $conf = @file_get_contents(self::config_path()); + + /* (3) Si erreur lecture */ + if( $conf === false ) + return $this->error->set(Err::UnreachableResource); + + /* (4) Parsage json */ + $this->modules = json_decode( $conf, true ); + + /* (5) Gestion de l'erreur de parsage */ + if( $this->modules == null ) + return $this->error->set(Err::ParsingFailed, 'json'); + + + + /* [3] Verification des types des parametres + =========================================================*/ + /* (1) Si path est une */ + if( !is_string($path) ) // Si le type est incorrect + return $this->error->set(Err::WrongPathModule); + + /* (2) Formattage @params en tableau */ + $params = (is_array($params)) ? $params : []; + + /* (3) On définit en constante la méthode HTTP */ + if( !isset($_SERVER['REQUEST_METHOD']) ) + return $this->error->set(Err::UnknownHttpMethod); + + $this->http_method = strtoupper($_SERVER['REQUEST_METHOD']); + + + /* [4] Verification du chemin (existence module+methode) + =========================================================*/ + if( !$this->checkPath($path) ) // Verification de la coherence du chemin + attribution + return false; // checkPath() sets the error itself + + + /* [5] Verification des droits + =========================================================*/ + if( !$this->checkPermission() ) // Si on a pas les droits + return false; // checkPermission() sets the error itself + + + /* [6] Verification des parametres (si @type est defini) + =========================================================*/ + if( !$this->checkParams($params) ) // Verification de tous les types + return false; // checkParams() sets the error itself + + + /* [7] Récupèration des options + =========================================================*/ + $this->buildOptions(); + + + /* [8] Construction de l'objet (add http method to params) + =========================================================*/ + $this->params = $params; + $this->params['HTTP_METHOD'] = $this->http_method; + $this->error->set(Err::Success); + + return true; // On retourne que tout s'est bien passe + } + + + + /* 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; + } + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET REMPLIE LA REPONSE + * + * @return answer Retourne une reponse de type si tout s'est bien passe + * + */ + public function dispatch(){ + /* [0] Si c'est un download, on lance la methode `download()` + =========================================================*/ + if( $this->options['download'] === true ) + return $this->download(); + + + /* [1] On verifie qu'aucune erreur n'a ete signalee + =========================================================*/ + if( $this->error->get() !== Err::Success ) // si il y a une erreur + return new Response($this->error); // on la passe a la reponse + + + /* [2] On essaie d'instancier le module + =========================================================*/ + $instance = ModuleFactory::getModule($this->path['module']); + + if( $instance instanceof Error ){ + $this->error->set(Err::UncallableModule, $this->path['module']); + return new Response($this->error); + } + + + /* [3] On verifie que la methode est amorcable + =========================================================*/ + if( !is_callable([$instance, $this->getModuleMethod()]) ){ + $this->error->set(Err::UncallableMethod, preg_replace('/\w+::/i', '', $this->path['method']) ); + return new Response($this->error); + } + + + /* [4] On amorce la methode + =========================================================*/ + /* (1) On lance la fonction */ + $returned = call_user_func( [$instance, $this->getModuleMethod()], $this->params ); + + /* (2) On appelle le destructeur (si défini) */ + $instance = null; + + + /* [5] Gestion de la reponse + =========================================================*/ + /* (1) On construit la réponse avec l'erreur */ + $response = new Response($this->error); + + /* (2) On ajoute les données */ + $response->appendAll($returned); + + // On retourne la réponse + return $response; + } + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET RENVOIE UN FICHIER AVEC LE HEADER ET LE BODY SPECIFIE + * + */ + public function download(){ + /* [1] On verifie qu'aucune erreur n'a ete signalee + =========================================================*/ + if( $this->error->get() !== Err::Success ) // si il y a une erreur + return new Response($this->error); // on la passe a la reponse + + + /* [2] On essaie d'instancier le module + =========================================================*/ + $instance = ModuleFactory::getModule($this->path['module']); + + if( $instance === false ){ + $this->error->set(Err::UncallableModule); + return new Response($this->error); + } + + /* [3] On verifie que la methode est amorcable + =========================================================*/ + if( !is_callable([$instance, $this->getModuleMethod()]) ){ + $this->error->set(Err::UncallableMethod); + return new Response($this->error); + } + + + /* [4] On amorce la methode + =========================================================*/ + /* (1) On lance la fonction */ + $returned = call_user_func( [$instance, $this->getModuleMethod()], $this->params ); + + /* (2) On appelle le destructeur (si défini) */ + $instance = null; + + + /* [5] Vérification des erreurs et paramètres + =========================================================*/ + /* (1) Vérification de l'erreur retournée, si pas Success, on retourne l'erreur */ + 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::ParamError); + 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']) ) + $returned['headers'] = []; + + $fromAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + + + /* [6.1] Si requête ajax on crée un fichier temporaire et on renvoie son URL + =========================================================*/ + if( $fromAjax ){ + + + $tmpfname = '/tmp/download_'.uniqid().'.php'; + $bodyfname = __ROOT__.'/tmp/content_'.uniqid().'.php'; + + /* (1) On crée le fichier temporaire */ + $tmpfnameroot = __PUBLIC__.$tmpfname; + $tmpfile = fopen($tmpfnameroot, 'w'); + + fwrite($tmpfile, '$value) + fwrite($tmpfile, "header(\"$header: $value\");".PHP_EOL); + + /* (3) Script qui écrira le contenu */ + // 1) On écrit le contenu dans un fichier temporaire (et oui encore) + $bodyfile = fopen($bodyfname, 'w'); + fwrite($bodyfile, $returned['body']); + fclose($bodyfile); + chmod($bodyfname, 0775); + + fwrite($tmpfile, "readfile('$bodyfname');".PHP_EOL); + + /* (4) Script qui supprimera les fichiers temporaires */ + fwrite($tmpfile, "unlink('$bodyfname');".PHP_EOL); + fwrite($tmpfile, "unlink(__FILE__);".PHP_EOL); + + fwrite($tmpfile, '?>'.PHP_EOL); + + /* (5) On ferme le fichier */ + fclose($tmpfile); + chmod($tmpfnameroot, 0775); + + $response = new Response($this->error); + $response->append('link', $tmpfname); + + return $response; + + /* [6.2] Gestion du download direct si pas AJAX + =========================================================*/ + }else{ + /* (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; + } + } + + + /* VERIFICATION DU FORMAT ET DE LA COHERENCE DU CHEMIN SPECIFIE + * + * @path String correspondant au chemin de delegation ("module/methode") + * + * @return validity Retourne si oui ou non l'objet est correct + * + */ + private function checkPath($path){ + /* [1] Verification format general + =========================================================*/ + if( !preg_match('@^([\w_-]+)/([\w_-]+)$@i', $path, $matches) ) // Si mauvais format + return $this->error->set(Err::WrongPathModule); + + // On recupere les données de la regex + $module = $matches[1]; + $method = $this->http_method.' '.$matches[2]; + + + /* [2] Verification de l'existence du module (conf) + =========================================================*/ + if( !array_key_exists($module, $this->modules) ) // Si le module n'est pas specifie dans la conf + return $this->error->set(Err::UnknownModule, $module); + + + /* [3] Verification de l'existence de la methode (conf) + =========================================================*/ + if( array_key_exists($method, $this->modules[$module]) === false ) // Si la methode n'est pas specifie dans la conf + return $this->error->set(Err::UnknownMethod, preg_replace('/\w+ /i', '', $method) ); + + + + /* [4] Enregistrement du chemin et renvoi de SUCCESS + =========================================================*/ + $this->path = [ + 'module' => $module, + 'method' => $method + ]; + + return true; + } + + + + + + + /* 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 = $this->modules[$this->path['module']][$this->path['method']]; + + // Si aucune permission n'est definie + if( !isset($method['permissions']) || !is_array($method['permissions']) || count($method['permissions']) < 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( $this->path['module'], $method['permissions'] ); + + /* (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; + } + + + + + /* VERIFICATION DU TYPE DES PARAMETRES ENVOYES + * + * @params Tableau associatif contenant les parametres + * @params peut se voir rajouter les paramètres optionnels s'ils ne sont pas renseignés (initialisés à NULL) + * + * @return correct Retourne si oui ou non les parametres ont le bon type + * + */ + private function checkParams(&$params){ + /* [1] On verifie qu'il ne manque aucun parametre + =========================================================*/ + /* (1) Si @params n'est pas un tableau */ + if( !is_array($params) ) + return $this->error->set(Err::MissingParam); + + /* (2) On récupère les données de la méthode */ + $method = $this->modules[$this->path['module']][$this->path['method']]; + + /* (3) Si pas 'parameters' dans la config */ + if( !isset($method['parameters']) || !is_array($method['parameters']) ) + return $this->error->set(Err::ConfigError); + + + /* [2] Si le type est defini, pour chaque param, on teste + =========================================================*/ + foreach($method['parameters'] as $name=>$paramsdata){ + + /* (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 @paramsdata n'est pas un tableau */ + if( !is_array($paramsdata) ) + return $this->error->set(Err::ConfigError); + + /* (3) So @paramsdata['type] manquant ou incorrect */ + if( !isset($paramsdata['type']) || !is_string($paramsdata['type']) ) + return $this->error->set(Err::ConfigError); + + + /* (2) Gestion des spécifications + ---------------------------------------------------------*/ + /* (1) On récupère si le paramètre est optionnel ou pas */ + $optional = isset($paramsdata['optional']) && $paramsdata['optional'] === true; + + /* (2) Si de type 'FILE' + fichier existe => on enregistre la ref. */ + if( $paramsdata['type'] == 'FILE' && isset($_FILES[$name]) ) + $params[$name] = &$_FILES[$name]; + + /* (3) Si param obligatoire et manquant -> erreur */ + if( !isset($params[$name]) && !$optional ) + return $this->error->set(Err::MissingParam, $name); + + + /* (3) Gestion des valeurs + ---------------------------------------------------------*/ + /* (1) Si le paramètre est optionnel et manquant */ + if( $optional && !isset($params[$name]) ){ + + // On le crée le param optionnel avec la valeur NULL + $params[$name] = null; + + /* (2) Si le paramètre est renseigné (sauf FILE) */ + }elseif( $paramsdata['type'] != 'FILE'){ + + // Si la verification est fausse, on retourne faux + if( !Checker::run($paramsdata['type'], $params[$name]) ) + return $this->error->set(Err::WrongParam, $name, $paramsdata['type']); + + } + + } + + /* [3] Gestion du retour, si tout s'est bien passe + =========================================================*/ + return true; + } + + + + + + /* AJOUT DES OPTIONS A PARTIR DE LA CONFIGURATION + * + */ + private function buildOptions(){ + /* [0] On récupère les options de la méthode en cours + =========================================================*/ + $method = $this->modules[$this->path['module']][$this->path['method']]; + + /* (1) Si 'option' n'est pas défini (ou incorrect), on met les valeurs par défaut */ + if( !isset($method['options']) || !is_array($method['options']) ) + 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['options']; + + + /* [1] 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; + + } + + + + + + /* RENVOI LE CHEMIN D'AMORCAGE DE LA METHODE + * + * @return method Retourne le chemin d'amorcage de la method + * + */ + private function getModuleMethod(){ + + /* (1) On essaie de trouver le bon nom */ + return preg_replace('/([A-Z]+) ([a-zA-Z_]+)/i', '$1_$2', $this->path['method']); + + } + + + } + +?> diff --git a/build/api/core/Response.php b/build/api/core/Response.php new file mode 100755 index 0000000..aaacd2e --- /dev/null +++ b/build/api/core/Response.php @@ -0,0 +1,132 @@ + 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/api/module/RESTexample.php b/build/api/module/RESTexample.php new file mode 100755 index 0000000..c73fe1f --- /dev/null +++ b/build/api/module/RESTexample.php @@ -0,0 +1,130 @@ + new Error(Err::ModuleError)]; // or other `Err` constant + + return [ + 'created_id' => "Article with title = `$title` and content=`$content`" + ]; + + } + + public function GET_article($argv){ + extract($argv); + + // GET all/an article with the variable: + $URL_0; // id of article ; if not given -> null + + // ... + // process to get articles and get $output_get_articles + $success = true; + // ... + + if( !$success ) + return ['error' => new Error(Err::ModuleError)]; // or other `Err` constant + + // optional param not given is set to null + if( is_null($URL_0) ) + return ['articles' => [ "Article number `1`: sometitle / somecontent", "Article number `2`: sometitle / somecontent"] ]; + else + return ['articles' => [ "Article number `$URL_0`: sometitle / somecontent"] ]; + } + + + public function VIEW_article($argv){ + extract($argv); + + // VIEW a specific article (download json file) with the variable: + $URL_0; // id of article ; if not given -> null + + // ... + // process to get articles and get $output_get_articles + $success = true; + // ... + + if( !$success ) + return ['error' => new Error(Err::ModuleError)]; // or other `Err` constant + + // will download, but if AJAX will give a `link` to the file + return [ + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'Content-Disposition' => 'attachment; filename=export'.date('_d_m_Y', time()).'.json', + 'Pragma' => 'no-cache', + 'Expires' => '0' + ], + 'body' => "Article number `$URL_0`: sometitle / somecontent" + ]; + + } + + public function PUT_article($argv){ + extract($argv); + + // UPDATE an article with new content with variables: + $URL_0; // id of article to update + $title; // new article's title + $content; // new article's content + + // ... + // process to get $output_updated_article + $success = true; + // ... + + if( !$success ) + return ['error' => new Error(Err::ModuleError)]; // or other `Err` constant + + return ['article' => "Article number `$URL_0`: $title / $content"]; + + } + + public function DELETE_article($argv){ + extract($argv); + + // DELETEs an article with the variable: + $URL_0; // id of the article to remove + + // ... + // process to delete article + $success = true; + // ... + + if( !$success ) + return ['error' => new Error(Err::ModuleError)]; // or other `Err` constant + + return ['log' => "Article `$URL_0` successfully deleted"]; // returns success + + } + + +} diff --git a/build/database/core/DatabaseDriver.php b/build/database/core/DatabaseDriver.php new file mode 100755 index 0000000..e8b94d3 --- /dev/null +++ b/build/database/core/DatabaseDriver.php @@ -0,0 +1,190 @@ + 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); + $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); + + // 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 getConfig(){ + return [ + 'host' => $this->host, + 'dbname' => $this->dbname, + 'username' => $this->username + ]; + } + + + + } +?> diff --git a/build/error/core/Err.php b/build/error/core/Err.php new file mode 100755 index 0000000..449733e --- /dev/null +++ b/build/error/core/Err.php @@ -0,0 +1,112 @@ + diff --git a/build/error/core/Error.php b/build/error/core/Error.php new file mode 100755 index 0000000..70f9944 --- /dev/null +++ b/build/error/core/Error.php @@ -0,0 +1,191 @@ + Const error + * @arg1 [OPT] Argument 1 + * @arg2 [OPT] Argument 2 + * @arg... [OPT] Argument ... + * + * @return instance Error instance + * + */ + public function __construct($const){ + call_user_func_array([$this, 'set'], func_get_args()); + } + + /* ERROR GETTER + * + * @return Err Error + * + */ + public function get(){ return $this->error; } + + /* ERROR SETTER + * + * @error Const error + * @arg1 [OPT] Argument 1 + * @arg2 [OPT] Argument 2 + * @arg... [OPT] Argument ... + * + * @return instance Error instance + * + */ + public function set($const){ + /* [1] On découpe les arguments + =========================================================*/ + /* (1) On récupère l'erreur */ + $this->error = !is_numeric($const) ? Err::UnknownError : $const; + + /* (2) On récupère les arguments */ + $this->arguments = array_slice(func_get_args(), 1); + } + + + /* EXPLICITE UN CODE D'ERREUR + * + * @return explicit Description explicite du code d'erreur + * + */ + public function explicit(){ + switch($this->error){ + case Err::Success: return $this->Success(); break; + case Err::ParsingFailed: return $this->ParsingFailed(); break; + case Err::UnreachableResource: return $this->UnreachableResource(); break; + case Err::UploadError: return $this->UploadError(); break; + case Err::FormatError: return $this->FormatError(); break; + case Err::TokenError: return $this->TokenError(); break; + case Err::PermissionError: return $this->PermissionError(); break; + case Err::DisabledModule: return $this->DisabledModule(); break; + case Err::MissingPath: return $this->MissingPath(); break; + case Err::WrongPathModule: return $this->WrongPathModule(); break; + case Err::UnknownModule: return $this->UnknownModule(); break; + case Err::UnknownMethod: return $this->UnknownMethod(); break; + case Err::UncallableModule: return $this->UncallableModule(); break; + case Err::UncallableMethod: return $this->UncallableMethod(); break; + case Err::UnknownHttpMethod: return $this->UnknownHttpMethod(); break; + case Err::ConfigError: return $this->ConfigError(); break; + case Err::MissingParam: return $this->MissingParam(); break; + case Err::WrongParam: return $this->WrongParam(); break; + case Err::ModuleError: return $this->ModuleError(); break; + case Err::PDOConnection: return $this->PDOConnection(); break; + case Err::WrongPathRepo: return $this->WrongPathRepo(); break; + case Err::UnknownRepo: return $this->UnknownRepo(); break; + case Err::RepoError: return $this->RepoError(); break; + case Err::UnknownTable: return $this->UnknownTable(); break; + case Err::NotAllowedSchema: return $this->NotAllowedSchema(); break; + case Err::NoMatchFound: return $this->NoMatchFound(); break; + case Err::UnknownTemplate: return $this->UnknownTemplate(); break; + case Err::UnknownAddress: return $this->UnknownAddress(); break; + case Err::UnknownError: return $this->UnknownError(); break; + + default: return $this->UnknownDebugError(); break; + } + } + + + private function Success(){ + return 'all right'; + }private function ParsingFailed(){ + if( count($this->arguments) > 0 ) + return $this->arguments[0].' parsing failed'; + else + return 'parsing failed'; + }private function UnreachableResource(){ + return 'unreachable resource'; + }private function UploadError(){ + return 'upload error'; + }private function FormatError(){ + return 'format error'; + }private function TokenError(){ + return 'bad or expired token'; + }private function PermissionError(){ + return 'permission error'; + }private function DisabledModule(){ + return 'disabled module'; + }private function MissingPath(){ + return 'missing path'; + }private function WrongPathModule(){ + return 'wrong module\'s path'; + }private function UnknownModule(){ + if( count($this->arguments) > 0 ) + return 'unknown module \''.$this->arguments[0].'\''; + else + return 'unknown module'; + }private function UnknownMethod(){ + if( count($this->arguments) > 0 ) + return 'unknown method \''.$this->arguments[0].'\''; + else + return 'unknown method'; + }private function UncallableModule(){ + if( count($this->arguments) > 0 ) + return 'uncallable module \''.$this->arguments[0].'\''; + else + return 'uncallable module'; + }private function UncallableMethod(){ + if( count($this->arguments) > 0 ) + return 'uncallable method \''.$this->arguments[0].'\''; + else + return 'uncallable method'; + }private function UnknownHttpMethod(){ + return 'unknown HTTP method'; + }private function ConfigError(){ + return 'configuration error'; + }private function MissingParam(){ + if( count($this->arguments) > 0 ) + return 'missing param \''.$this->arguments[0].'\''; + else + return 'missing param'; + }private function WrongParam(){ + if( count($this->arguments) > 0 ) + if( count($this->arguments) > 1 ) + return 'wrong param \''.$this->arguments[0].'\' expected to be of type \''.$this->arguments[1].'\''; + else + return 'wrong param \''.$this->arguments[0].'\''; + else + return 'wrong param'; + }private function ModuleError(){ + return 'module error'; + }private function PDOConnection(){ + return 'database error'; + }private function WrongPathRepo(){ + return 'wrong repository\'s path'; + }private function UnknownRepo(){ + return 'unknown repository'; + }private function RepoError(){ + return 'repository error'; + }private function UnknownTable(){ + return 'unknown table'; + }private function NotAllowedSchema(){ + return 'schema browsing not allowed'; + }private function NoMatchFound(){ + return 'no match found'; + }private function UnknownTemplate(){ + return 'unknown template'; + }private function UnknownAddress(){ + return 'unknown'; + }private function UnknownError(){ + return 'unknown error'; + }private function UnknownDebugError(){ + return 'unknown debug error'; + } + + + public function setHttpCode(){ + http_response_code( $this->error == Err::Success ? 200 : 417 ); + } + + } + + +?> diff --git a/build/http/core/HttpRequest.php b/build/http/core/HttpRequest.php new file mode 100755 index 0000000..051dfe6 --- /dev/null +++ b/build/http/core/HttpRequest.php @@ -0,0 +1,208 @@ + auto-filled HTTP Request + * + =========================================================*/ + public function __construct(){ + /* [1] Define URI & Status Code & method + =========================================================*/ + $this->uri = $_SERVER['REQUEST_URI']; + $this->method = $_SERVER['REQUEST_METHOD']; + + + /* [2] Define headers + =========================================================*/ + $this->headers = \getallheaders(); + + + /* [3] Define default datasets (GET, POST) + =========================================================*/ + $this->getdata = $_GET; + $this->postdata = $_POST; + + + /* [4] Define BODY & its type + =========================================================*/ + /* (1) Default: set plain/text body */ + $this->body = \file_get_contents('php://input'); + + /* (2) Fetch content type */ + $this->type = self::getContentType($this->headers['Content-Type']); + + + /* [5] Parse BODY data -> POST + =========================================================*/ + $this->parseBody(); + } + + + + /* GET CONSTANT CT_* FROM `Content-Type` HEADER + * + * @pContentType `Content-Type` header value + * + * @return type Constant value + * + */ + private static function getContentType($pContentType=null){ + /* [1] Checks argv + =========================================================*/ + if( is_null($pContentType) ) + $pContentType = $_SERVER['CONTENT_TYPE']; + + + /* [2] Checks types + =========================================================*/ + /* (1) Form Data Types + ---------------------------------------------------------*/ + /* (1) multipart/form-data */ + if( preg_match('/^multipart\/form\-data; boundary=(.+)$/i', $pContentType) ) + return self::CT_MULTIPART_FORM_DATA; + + /* (2) application/x-www-form-urlencoded */ + if( preg_match('/^application\/x\-www\-form\-urlencoded/i', $pContentType) ) + return self::CT_X_WWW_FORM_URLENCODED; + + + /* (2) Data types + ---------------------------------------------------------*/ + /* (1) Basic JSON content type */ + if( preg_match('/^application\/json/i', $pContentType) ) + return self::CT_JSON; + + /* (2) Basic YAML content type */ + if( preg_match('/^application\/yaml/i', $pContentType) ) + return self::CT_YAML; + + /* (3) Basic TEXT content type */ + if( preg_match('/text\/[a-z]+/', $pContentType) ) + return self::CT_TEXT; + + + /* (3) Default Type + ---------------------------------------------------------*/ + return self::CT_BINARY; + + } + + + + /* PARSES BODY DATA + * + */ + private function parseBody(){ + /* [1] If empty body -> do nothing + =========================================================*/ + if( strlen($this->body) === 0 ) + return true; + + + /* [2] Management for each ContentType + =========================================================*/ + switch($this->type){ + + /* (1) multipart/form-data -> parse for not-POST methods + ---------------------------------------------------------*/ + case self::CT_MULTIPART_FORM_DATA: + /* (1) Fetch the boundary */ + if( !preg_match('/boundary=(.+)$/i', $this->headers['Content-Type'], $match) ) + return false; + + $boundary = $match[1]; + + /* (2) Break body into parts */ + $splitter = "/(?:\n|\r\n|--)*$boundary(?:\n|\r\n|--)?/im"; + $parts = preg_split($splitter, $this->body); + + /* (3) Process parts */ + foreach($parts as $part) + if( preg_match('/^Content\-Disposition: form\-data; name=\"([^"]+)\"(?:\n|\r\n){2}(.+)$/mi', $part, $match) ) + $this->postdata[$match[1]] = $match[2]; + + /* (4) Erases body */ + $this->body = ''; + break; + + + /* (2) application/x-www-form-urlencoded -> parse for not-POST methods + ---------------------------------------------------------*/ + case self::CT_X_WWW_FORM_URLENCODED: + /* Auto parse builtin-php function */ + parse_str($this->body, $this->postdata); + + /* Erases body */ + $this->body = ''; + break; + + + /* (3) application/json -> parse if no error + ---------------------------------------------------------*/ + case self::CT_JSON: + /* (1) Decode body content */ + $decoded = json_decode($this->body, true); + + /* (2) If error -> do nothing */ + if( is_null($decoded) ) + return; + + /* (3) Parse body into body */ + $this->body = $decoded; + break; + + + /* (4) application/yaml -> parse if no error + ---------------------------------------------------------*/ + case self::CT_YAML: + + break; + } + } + + + public function BODY(){ return $this->body; } + public function POST(){ return $this->postdata; } + public function GET(){ return $this->getdata; } + public function HEADERS(){ return $this->headers; } + public function METHOD(){ return $this->method; } + public function URI(){ return $this->uri; } + } diff --git a/build/router/controller/api.php b/build/router/controller/api.php new file mode 100755 index 0000000..6cf1106 --- /dev/null +++ b/build/router/controller/api.php @@ -0,0 +1,56 @@ + Calling URI + * + */ + public function __construct($matches){ + + /* (1) Rebuild request url */ + $uri = $matches['module'].'/'.$matches['method'].$matches['uri_args']; + + /* (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 100755 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 100755 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 100755 index 0000000..679e927 --- /dev/null +++ b/build/router/core/ControllerFactory.php @@ -0,0 +1,61 @@ + 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 100755 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 100755 index 0000000..8873dfa --- /dev/null +++ b/build/router/core/Router.php @@ -0,0 +1,335 @@ +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 */ + // /*DEBUG*/ var_dump($pattern); + $pattern = self::formatPattern($pattern, $arguments); + // /*DEBUG*/ var_dump($pattern); + + 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\.]*(?:\{[a-z_]+\})*[\w\.]*)*\/?$/i', $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/.htaccess b/config/.htaccess new file mode 100755 index 0000000..93169e4 --- /dev/null +++ b/config/.htaccess @@ -0,0 +1,2 @@ +Order deny,allow +Deny from all diff --git a/config/database-driver.json b/config/database-driver.json new file mode 100755 index 0000000..4f51030 --- /dev/null +++ b/config/database-driver.json @@ -0,0 +1,16 @@ +{ + "default": { + "local": { + "host" : "db_local_host", + "dbname" : "db_local_name", + "user" : "db_local_user", + "password" : "db_local_password" + }, + "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 100755 index 0000000..ad23777 --- /dev/null +++ b/config/modules.json @@ -0,0 +1,60 @@ +{ + + "RESTexample": { + "POST article": { + "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 article": { + "description": "Gets all or a specific article", + "permissions": ["viewer", "journalist"], + "parameters": { + "URL_0": { "description": "Article id", "type": "id", "optional": true } + }, + "output": { + "articles": { "description": "List of selected articles", "type": "array" } + } + }, + + "VIEW article": { + "description": "Gets a specific article into a json file (download)", + "permissions": ["viewer", "journalist"], + "options": { "download": true }, + "parameters": { + "URL_0": { "description": "Article id", "type": "id" } + }, + "output": { + "article": { "description": "Selected article as JSON file", "type": "text" } + } + }, + + "PUT article": { + "description": "Updates a specific article", + "permissions": ["journalist"], + "parameters": { + "URL_0": { "description": "Article id", "type": "id" }, + "content": { "description": "Article's content", "type": "text" } + }, + "output": { + "article": { "description": "Returns updated article", "type": "array" } + } + }, + + "DELETE article": { + "description": "Deletes a specific article", + "permissions": ["journalist"], + "parameters": { + "URL_0": { "description": "Article id", "type": "id" } + }, + "output": {} + } + } +} diff --git a/config/routes.json b/config/routes.json new file mode 100755 index 0000000..c1538f2 --- /dev/null +++ b/config/routes.json @@ -0,0 +1,36 @@ +{ + + "methods": [ "GET", "POST", "PUT", "DELETE" ], + + + "routes": { + + "/{page}/": { + "methods": ["GET"], + "controller": "page:load", + "arguments": { + "page": "[a-z]+" + } + }, + + "/api/v/1.0/{module}/{method}{uri_args}": { + "methods": ["GET", "POST", "PUT", "DELETE"], + "controller": "api:call", + "arguments": { + "module": "[a-zA-Z_]+", + "method": "[a-zA-Z_]+", + "uri_args": "(\\/[\\w:-]+)*\\/?" + } + }, + + "/{any}": { + "methods": ["GET"], + "controller": "redirect:homepage", + "arguments": { + "any": ".*" + } + } + + } + +} diff --git a/public_html/.htaccess b/public_html/.htaccess new file mode 100755 index 0000000..760dba5 --- /dev/null +++ b/public_html/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine on + +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^(.*)$ index.php?url=/$1 [QSA,L] diff --git a/public_html/index.php b/public_html/index.php new file mode 100755 index 0000000..7ecd706 --- /dev/null +++ b/public_html/index.php @@ -0,0 +1,6 @@ +