Updated api:3.0
This commit is contained in:
parent
410db63941
commit
bfeee3358d
|
@ -42,45 +42,42 @@ Links
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> # **I.** Overview
|
# **I.** Overview
|
||||||
|
|
||||||
> ## **1** Introduction & features
|
## **1** Introduction & features
|
||||||
|
|
||||||
The `api` package (v2.2) allows you to easily create and manage an API. It could be used for an HTTP API (REST, or other kind), or you can use it as an internal core for your system.
|
The `api` package (v3.0) allows you to easily create and manage an API for your applications. It can be used as an HTTP API (REST, or other kind), and you can use it as an internal core for your system.
|
||||||
|
|
||||||
The aim of this package is to make your life easier working with APIs or internal delegation. The only things you have to do is to implement your processes and edit the configuration, the package will do the rest.
|
The aim of this package is to make your life easier working with APIs and internal delegation. The only things you have to do is to implement your processes and write the configuration, the package will do the rest.
|
||||||
|
|
||||||
Things you have to do :
|
Things you **have** to do :
|
||||||
- implement your processes (obviously)
|
- implement your processes (obviously)
|
||||||
- implement your authentication system (cf. [AuthSystem](#1-permissions--authsystem))
|
- implement your authentication system (cf. [AuthSystem](#1-permissions--authsystem))
|
||||||
- edit the configuration file (cf. [configuration](#iii-configuration))
|
- setup the configuration file (cf. [configuration](#iii-configuration))
|
||||||
|
|
||||||
Things you **don't have** to do :
|
Things you **don't have** to do :
|
||||||
- input type check (cf. [Checker](#4-checker))
|
- input type check (cf. [Checker](#4-checker))
|
||||||
- API multiple permission management
|
- multiple permission management
|
||||||
- optional or required inputs
|
- optional or required input
|
||||||
|
- multipart and file input
|
||||||
|
- URL input
|
||||||
- before and after scripts
|
- before and after scripts
|
||||||
- catch both in-URL and `multipart/form-data` input
|
- and a lot more ...
|
||||||
|
|
||||||
> ## **2** Basic knowledge
|
> ## **2** Basic knowledge
|
||||||
|
|
||||||
The API is based over a 2-level delegation structure :
|
The api is made of paths that binds to a *php class*, each `path` can manage multiple **HTTP METHODS** each will correspond to a *method* of the bound class.
|
||||||
1. `module` which is a set of methods
|
|
||||||
2. `method` which have input, output, permissions, and is bound to a function
|
|
||||||
|
|
||||||
So each of your functionalities must have a `method` name and be inside a `module`.
|
So each of your functionalities must have a `method` and a corresponding a `path`.
|
||||||
|
|
||||||
<u>Example:</u>
|
<u>Example:</u>
|
||||||
* the module `article` contains methods:
|
* the module `article` contains methods:
|
||||||
* `read` with argument `article_id` (to identify the wanted article)
|
* `GET` to get article data
|
||||||
* `write` with arguments `title` and `body` (data to write into the new created article)
|
* `POST` to post a new article
|
||||||
* `edit` with arguments `article_id` and `body` (to identify and replace the body)
|
* `PUT` to edit an existing article
|
||||||
* `delete` with argument `article_id` (to identify which article to delete)
|
* `DELETE` to delete an exisint article
|
||||||
|
|
||||||
If you want to delete the article of id `52`, you must request `article/delete` passing `article_id`=`52`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*Note that each method must be a valid HTTP METHOD*.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -125,11 +122,11 @@ use \error\core\Err;
|
||||||
> ### 1) create a request
|
> ### 1) create a request
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// creates a request for the module {module} and its method {method} with params
|
// creates a request @path with params
|
||||||
$request = new Request('{module}/{method}', [
|
$request = new \api\core\Request('some/path', [
|
||||||
'param1' => 10,
|
'param1' => 10,
|
||||||
'param2' => 'somevalue'
|
'param2' => 'somevalue'
|
||||||
]);
|
], 'POST'); // wanted HTTP method to emulate
|
||||||
```
|
```
|
||||||
|
|
||||||
> ### 2) catch possible errors (optional)
|
> ### 2) catch possible errors (optional)
|
||||||
|
@ -169,20 +166,20 @@ $specific = $response->get('someOutputName');
|
||||||
In order to setup an automatic bound from HTTP requests to API directly, you must use a **router**.
|
In order to setup an automatic bound from HTTP requests to API directly, you must use a **router**.
|
||||||
|
|
||||||
|
|
||||||
> ### 1) Format url so it must begin with `/{module}/{method}`
|
> ### 1) Format url so it must begin with `/@path`
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// let's suppose the url is `/api/{module}/{method}`
|
// let's suppose the url is `/api/@path`
|
||||||
$url = '/api/somemodule/somemethod/1/2/';
|
$url = '/api/some/path/1/2/';
|
||||||
$uri = substr($url, strlen('/api'));
|
$uri = substr($url, strlen('/api'));
|
||||||
// $uri = /somemodule/somemethod/1/2/
|
// $uri = /some/path/1/2/
|
||||||
```
|
```
|
||||||
|
|
||||||
> ### 2) give the url to the HTTP manager
|
> ### 2) give the url to the HTTP manager
|
||||||
|
|
||||||
```php
|
```php
|
||||||
// create request from HTTP data
|
// create request from HTTP data
|
||||||
$request = Request::remote($url);
|
$request = \api\core\Loader::remote($uri);
|
||||||
|
|
||||||
// execute request and catch response
|
// execute request and catch response
|
||||||
// note that request errors will propagate through response
|
// note that request errors will propagate through response
|
||||||
|
@ -201,33 +198,26 @@ Then can handle various kinds of URL :
|
||||||
- request and parameters of both URL, post data, and form-data are caught
|
- request and parameters of both URL, post data, and form-data are caught
|
||||||
|
|
||||||
### The following examples can work :
|
### The following examples can work :
|
||||||
> 1. `http://www.host.com/{module}/{method}/`
|
> 1. `http://www.host.com/@path/{param3}`
|
||||||
```json
|
```json
|
||||||
"post-data": {
|
"post-data": {
|
||||||
"param1": "{value1}",
|
"param1": "{value1}",
|
||||||
"param2": "{value2}"
|
"param2": "{value2}"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
> 2. `http://www.host.com/{module}/{method}/{param1}/{param2}`
|
|
||||||
|
> 2. `http://www.host.com/@path/{param1}/{param2}`
|
||||||
```json
|
```json
|
||||||
"post-data": {}
|
"post-data": {}
|
||||||
```
|
```
|
||||||
> 3. `http://www.host.com/apiOrParentUrl/`
|
|
||||||
|
> 3. `http://www.host.com/@path/1,`
|
||||||
```json
|
```json
|
||||||
"post-data": {
|
"post-data": {
|
||||||
"module": "{module}",
|
|
||||||
"method": "{method}",
|
|
||||||
"param1": "{value1}",
|
"param1": "{value1}",
|
||||||
"param2": "{value2}"
|
"param2": "{value2}"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
> 4. `http://www.host.com/apiOrParentUrl/{value1}/{value2}`
|
|
||||||
```json
|
|
||||||
"post-data": {
|
|
||||||
"module": "{module}",
|
|
||||||
"method": "{method}",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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<mixed>" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"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<mixed>" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,20 @@
|
||||||
<?php require_once '../autoloader.php';
|
<?php require_once '../autoloader.php';
|
||||||
|
|
||||||
use \router\core\Router;
|
use \router\core\Router;
|
||||||
|
use \api\core\Request;
|
||||||
|
use \api\core\AuthSystemDefault;
|
||||||
|
use \database\core\Repo;
|
||||||
|
use \database\core\DatabaseDriver;
|
||||||
|
|
||||||
|
/* (1) Start session */
|
||||||
|
session_start();
|
||||||
|
|
||||||
$R = new Router( $_GET['url'] );
|
/* (2) Set default Driver for Repos */
|
||||||
$R->run();
|
Repo::setDriver(DatabaseDriver::get());
|
||||||
|
|
||||||
|
/* (3) Set default AuthSystem */
|
||||||
|
Request::setAuthSystem(new AuthSystemDefault);
|
||||||
|
|
||||||
|
/* (4) launch router */
|
||||||
|
Router::launch($_SERVER['REQUEST_URI']);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace api\core;
|
||||||
|
|
||||||
|
use \error\core\Error;
|
||||||
|
use \error\core\Err;
|
||||||
|
|
||||||
|
|
||||||
|
class Config{
|
||||||
|
|
||||||
|
/* (1) Attributes
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
/* (1) Static */
|
||||||
|
private static $inst = null; // singleton instance
|
||||||
|
private static function config_path(){ return __ROOT__.'/config/modules.json'; }
|
||||||
|
public static $allowed_http_methods = [ "GET", "POST", "PUT", "DELETE" ];
|
||||||
|
|
||||||
|
/* (2) Instance */
|
||||||
|
private $raw;
|
||||||
|
public $index;
|
||||||
|
public $error;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* (2) Private constructor
|
||||||
|
*
|
||||||
|
* @path<String> 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<Config> 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace api\core;
|
||||||
|
|
||||||
|
use \error\core\Error;
|
||||||
|
use \error\core\Err;
|
||||||
|
use \api\core\Request;
|
||||||
|
use \api\core\Config;
|
||||||
|
|
||||||
|
|
||||||
|
class Documentation{
|
||||||
|
|
||||||
|
/* (1) Attributes
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
/* (1) Static */
|
||||||
|
|
||||||
|
/* (2) Instance */
|
||||||
|
|
||||||
|
|
||||||
|
/* (2) Builds the documentation
|
||||||
|
*
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
public function generate(Request $rq=null){
|
||||||
|
|
||||||
|
/* (1) Get data from config
|
||||||
|
---------------------------------------------------------*/
|
||||||
|
/* (1) If no index for this path -> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -27,18 +27,21 @@
|
||||||
/* (1) On gère les arguments */
|
/* (1) On gère les arguments */
|
||||||
$arguments = is_array($arguments) ? $arguments : [];
|
$arguments = is_array($arguments) ? $arguments : [];
|
||||||
|
|
||||||
/* (2) On transforme @module en namespace */
|
/* (2) Exception: URI Racine */
|
||||||
|
if( $module == '/' )
|
||||||
|
$module = '/root';
|
||||||
|
|
||||||
|
/* (3) On transforme @module en namespace */
|
||||||
$module_ns = str_replace('/', '\\', $module);
|
$module_ns = str_replace('/', '\\', $module);
|
||||||
|
|
||||||
|
/* (4) On vérifie que la classe existe */
|
||||||
/* (1) On vérifie que la classe existe */
|
if( !file_exists(__BUILD__."/api/module$module.php") )
|
||||||
if( !file_exists(__BUILD__."/api/module/$module.php") )
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* (2) On récupère la classe */
|
/* (5) On récupère la classe */
|
||||||
$class_name = "\\api\\module\\$module_ns";
|
$class_name = "\\api\\module$module_ns";
|
||||||
|
|
||||||
/* (3) On retourne une instance */
|
/* (6) On retourne une instance */
|
||||||
return new $class_name($arguments);
|
return new $class_name($arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
use \database\core\DatabaseDriver;
|
use \database\core\DatabaseDriver;
|
||||||
use \api\core\AuthSystem;
|
use \api\core\AuthSystem;
|
||||||
use \api\core\ModuleFactory;
|
use \api\core\ModuleFactory;
|
||||||
|
use \api\core\Config;
|
||||||
use \error\core\Error;
|
use \error\core\Error;
|
||||||
use \error\core\Err;
|
use \error\core\Err;
|
||||||
|
|
||||||
|
@ -11,24 +12,17 @@
|
||||||
class Request{
|
class Request{
|
||||||
|
|
||||||
// Constantes
|
// Constantes
|
||||||
public static function config_path(){ return __ROOT__.'/config/modules.json'; }
|
|
||||||
private static $default_options = [ 'download' => false ];
|
private static $default_options = [ 'download' => false ];
|
||||||
private static $authsystem = null;
|
private static $authsystem = null;
|
||||||
// liste des methodes HTTP autorisées
|
|
||||||
private static $allowed_http_methods = [ "GET", "POST", "PUT", "DELETE" ];
|
|
||||||
|
|
||||||
|
|
||||||
// Attributs prives utiles (initialisation)
|
// Attributs prives utiles (initialisation)
|
||||||
private $path; // chemin de base (uri)
|
private $id; // chemin extrait de l'URI
|
||||||
private $raw_params; // paramètres reçus
|
private $raw_params; // paramètres reçus
|
||||||
private $params; // paramètres donnés à la fonction
|
private $params; // paramètres donnés à la fonction
|
||||||
private $schema; // schema configuration
|
|
||||||
private $options; // options
|
private $options; // options
|
||||||
private $http_method; // methode HTTP appelante
|
private $http_method; // methode HTTP appelante
|
||||||
|
|
||||||
// Contiendra la reponse a la requete
|
|
||||||
public $answer;
|
|
||||||
|
|
||||||
// Contiendra l'etat de la requete
|
// Contiendra l'etat de la requete
|
||||||
public $error;
|
public $error;
|
||||||
|
|
||||||
|
@ -67,18 +61,15 @@
|
||||||
$this->error = new Error(Err::Success);
|
$this->error = new Error(Err::Success);
|
||||||
|
|
||||||
/* (2) Si pas parametre manquant, on quitte */
|
/* (2) Si pas parametre manquant, on quitte */
|
||||||
if( $uri == null )
|
if( is_null($uri) )
|
||||||
return $this->error->set(Err::MissingPath);
|
return $this->error->set(Err::MissingPath);
|
||||||
|
|
||||||
|
|
||||||
/* (2) On met a jour la configuration
|
/* (2) On vérifie la configuration
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
/* (1) Build from configuration */
|
/* (1) Dispatch if error */
|
||||||
$this->buildConfig();
|
if( Config::get()->error->get() != Err::Success )
|
||||||
|
return ($this->error = Config::get()->error);
|
||||||
/* (2) Dispatch if error */
|
|
||||||
if( $this->error->get() != Err::Success )
|
|
||||||
return;
|
|
||||||
|
|
||||||
|
|
||||||
/* (3) Verification des types des parametres
|
/* (3) Verification des types des parametres
|
||||||
|
@ -87,45 +78,54 @@
|
||||||
if( !is_string($uri) ) // Si le type est incorrect
|
if( !is_string($uri) ) // Si le type est incorrect
|
||||||
return $this->error->set(Err::WrongPathModule);
|
return $this->error->set(Err::WrongPathModule);
|
||||||
|
|
||||||
/* (2) Formattage @params en tableau */
|
/* (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 : [];
|
$this->raw_params = (is_array($params)) ? $params : [];
|
||||||
|
|
||||||
/* (3) On définit en constante la méthode HTTP */
|
/* (4) On définit en constante la méthode HTTP */
|
||||||
if( !isset($_SERVER['REQUEST_METHOD']) && !is_string($forced_method) )
|
if( !isset($_SERVER['REQUEST_METHOD']) && !is_string($forced_method) )
|
||||||
return $this->error->set(Err::UnknownHttpMethod);
|
return $this->error->set(Err::UnknownHttpMethod);
|
||||||
|
|
||||||
$this->http_method = is_string($forced_method) ? strtoupper($forced_method) : strtoupper($_SERVER['REQUEST_METHOD']);
|
$this->http_method = is_string($forced_method) ? strtoupper($forced_method) : strtoupper($_SERVER['REQUEST_METHOD']);
|
||||||
|
|
||||||
|
|
||||||
/* (4) Verification du chemin (existence module+methode)
|
/* (4) Verification du chemin (existence module+methode)
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
if( !$this->checkURI($uri) ) // Verification de la coherence du chemin + attribution
|
if( !$this->checkURI($uri) ) // Verification de la coherence du chemin + attribution
|
||||||
return false; // checkURI() sets the error itself
|
return false; // checkURI() sets the error itself
|
||||||
|
|
||||||
|
|
||||||
/* (5) Verification des permissions
|
/* (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
|
if( !$this->checkPermission() ) // Si on a pas les droits
|
||||||
return false; // checkPermission() sets the error itself
|
return false; // checkPermission() sets the error itself
|
||||||
|
|
||||||
|
|
||||||
/* (6) Verification des parametres (si @type est defini)
|
/* (7) Verification des parametres (si @type est defini)
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
if( !$this->checkParams() ) // Verification de tous les types
|
if( !$this->checkParams() ) // Verification de tous les types
|
||||||
return false; // checkParams() sets the error itself
|
return false; // checkParams() sets the error itself
|
||||||
|
|
||||||
|
|
||||||
/* (7) Récupèration des options
|
/* (8) Récupèration des options
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
$this->buildOptions();
|
$this->buildOptions();
|
||||||
|
|
||||||
|
|
||||||
/* (8) Construction de l'objet (add http method to params)
|
/* (9) Construction de l'objet
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
$this->params['HTTP_METHOD'] = $this->http_method;
|
|
||||||
$this->error->set(Err::Success);
|
$this->error->set(Err::Success);
|
||||||
|
|
||||||
return true; // On retourne que tout s'est bien passe
|
return true; // On retourne que tout s'est bien passe
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,67 +150,9 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (3) Construction du schéma à partir de la configuration
|
/* (3) Verification du format et de la coherence du chemin specifie
|
||||||
*
|
*
|
||||||
---------------------------------------------------------*/
|
* @URI<String> URI d'appel (commence par /)
|
||||||
private function buildConfig(){
|
|
||||||
|
|
||||||
/* (1) Access file content
|
|
||||||
---------------------------------------------------------*/
|
|
||||||
/* (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->schema['raw'] = json_decode( $conf, true );
|
|
||||||
|
|
||||||
/* (5) Gestion de l'erreur de parsage */
|
|
||||||
if( $this->schema['raw'] == null )
|
|
||||||
return $this->error->set(Err::ParsingFailed, 'json');
|
|
||||||
|
|
||||||
|
|
||||||
/* (2) Construction des outils d'accès
|
|
||||||
---------------------------------------------------------*/
|
|
||||||
/* (1) Initialisation */
|
|
||||||
$this->schema['index'] = [];
|
|
||||||
|
|
||||||
/* (2) Pour chaque chemin */
|
|
||||||
foreach($this->schema['raw'] as $path=>$methods){
|
|
||||||
|
|
||||||
/* (2.1) Pour chaque méthode */
|
|
||||||
foreach($methods as $method=>$data){
|
|
||||||
|
|
||||||
/* (2.1.1) Suppression si pas dans les méthodes autorisées */
|
|
||||||
if( !in_array($method, self::$allowed_http_methods) ){
|
|
||||||
unset($this->schema[$path][$method]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (2.1.2) Création de l'index pour le chemin si n'existe pas déja */
|
|
||||||
if( !isset($this->schema['index'][$path]) )
|
|
||||||
$this->schema['index'][$path] = [];
|
|
||||||
|
|
||||||
/* (2.1.3) Ajout de la méthode à l'index */
|
|
||||||
$this->schema['index'][$path][] = $method;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (4) Verification du format et de la coherence du chemin specifie
|
|
||||||
*
|
|
||||||
* @path<String> String correspondant au chemin de delegation ("module/methode")
|
|
||||||
*
|
*
|
||||||
* @return validity<Boolean> Retourne si oui ou non l'objet est correct
|
* @return validity<Boolean> Retourne si oui ou non l'objet est correct
|
||||||
*
|
*
|
||||||
|
@ -220,9 +162,14 @@
|
||||||
/* (1) Verification format general
|
/* (1) Verification format general
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
/* (1) If wrong format -> exit */
|
/* (1) If wrong format -> exit */
|
||||||
if( !preg_match('@^\w+(\/\w+)*\/?$@', $uri, $matches) )
|
if( !preg_match('@^\/[^\/]*(\/[^\/]*)*\/?$@', $uri) )
|
||||||
return $this->error->set(Err::WrongPathModule);
|
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)
|
/* (2) Verification de l'existence du chemin (conf)
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
|
@ -230,12 +177,12 @@
|
||||||
$exists_size = 0;
|
$exists_size = 0;
|
||||||
$path = null;
|
$path = null;
|
||||||
|
|
||||||
foreach($this->schema['index'] as $key=>$void){
|
foreach(Config::get()->index as $key=>$void){
|
||||||
$match_size = strlen($key);
|
$match_size = strlen($key);
|
||||||
|
|
||||||
/* (1.1) Look for the longer match */
|
/* (1.1) Look for the longer ( match | '/' ) */
|
||||||
if( $match_size > $exists_size && substr($uri, 0, $match_size) == $key ){
|
if( $match_size > $exists_size && substr($uri, 0, $match_size+1) == "$key/" || $key == '/' ){
|
||||||
$exists_size = $match_size;
|
$exists_size = $key == '/' ? 0 : $match_size;
|
||||||
$path = $key;
|
$path = $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,33 +198,43 @@
|
||||||
/* (1) Extract URI string after @path */
|
/* (1) Extract URI string after @path */
|
||||||
$uri_end = substr($uri, $exists_size);
|
$uri_end = substr($uri, $exists_size);
|
||||||
|
|
||||||
/* (2) If invalid format, return error */
|
/* (2) Special case: add / if root uri arguments */
|
||||||
if( !preg_match('@^((?:\/[^\/]+)*)\/?$@', $uri_end, $uri_match) )
|
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);
|
return $this->error->set(Err::InvalidURI);
|
||||||
|
|
||||||
/* (3) Add each URI parameter to the parameter store */
|
/* (4) Add each URI parameter to the parameter store */
|
||||||
$uri_args = array_slice( explode('/', $uri_match[1]), 1);
|
$uri_args = array_slice( explode('/', $uri_match[1]), 1);
|
||||||
|
|
||||||
foreach($uri_args as $index=>$value)
|
foreach($uri_args as $index=>$value)
|
||||||
$this->raw_params["URL$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)
|
/* (4) Verification de l'existence de la methode (conf)
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
/* (1) Check if HTTP method is in allowed methods */
|
/* (1) If it is a documentation request */
|
||||||
if( !in_array($this->http_method, self::$allowed_http_methods) )
|
$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);
|
return $this->error->set(Err::UnknownHttpMethod, $this->http_method);
|
||||||
|
|
||||||
/* (2) Check if HTTP method is defined for this @path */
|
/* (3) Check if HTTP method is defined for this @path */
|
||||||
if( !in_array($this->http_method, $this->schema['index'][$path]) )
|
if( !isset(Config::get()->index[$path][$this->http_method]) && !$doc_req )
|
||||||
return $this->error->set(Err::UnknownMethod, $this->http_method);
|
return $this->error->set(Err::UnknownMethod, $this->http_method);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (5) Enregistrement du chemin et renvoi de SUCCESS
|
/* (5) Enregistrement du chemin et renvoi de SUCCESS
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
$this->path = [
|
$this->id = [
|
||||||
'path'=> $path,
|
'path' => $path,
|
||||||
'method'=> $this->http_method
|
'method' => $this->http_method,
|
||||||
|
'doc_request' => $doc_req
|
||||||
];
|
];
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -285,7 +242,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (5) Retourne si on a la permission d'executer cette methode
|
/* (4) Retourne si on a la permission d'executer cette methode
|
||||||
*
|
*
|
||||||
* @return permission<bool> Retourne si on a les droits ou pas pour executer cette methode
|
* @return permission<bool> Retourne si on a les droits ou pas pour executer cette methode
|
||||||
*
|
*
|
||||||
|
@ -295,7 +252,7 @@
|
||||||
/* (1) On recupere les informations utiles
|
/* (1) On recupere les informations utiles
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
// On recupere le nom de la methode
|
// On recupere le nom de la methode
|
||||||
$method = $this->schema['raw'][$this->path['path']][$this->path['method']];
|
$method = Config::get()->index[$this->id['path']][$this->id['method']];
|
||||||
|
|
||||||
// Si aucune permission n'est definie
|
// Si aucune permission n'est definie
|
||||||
if( !isset($method['permissions']) || !is_array($method['permissions']) || count($method['permissions']) < 1 )
|
if( !isset($method['permissions']) || !is_array($method['permissions']) || count($method['permissions']) < 1 )
|
||||||
|
@ -332,7 +289,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (6) Verification du type des parametres envoyes
|
/* (5) Verification du type des parametres envoyes
|
||||||
*
|
*
|
||||||
* @return correct<bool> Retourne si oui ou non les parametres ont le bon type
|
* @return correct<bool> Retourne si oui ou non les parametres ont le bon type
|
||||||
*
|
*
|
||||||
|
@ -346,7 +303,7 @@
|
||||||
return $this->error->set(Err::MissingParam);
|
return $this->error->set(Err::MissingParam);
|
||||||
|
|
||||||
/* (2) On récupère les données de la méthode */
|
/* (2) On récupère les données de la méthode */
|
||||||
$method = $this->schema['raw'][$this->path['path']][$this->path['method']];
|
$method = Config::get()->index[$this->id['path']][$this->id['method']];
|
||||||
|
|
||||||
/* (3) Si pas 'parameters' dans la config */
|
/* (3) Si pas 'parameters' dans la config */
|
||||||
if( !isset($method['parameters']) || !is_array($method['parameters']) )
|
if( !isset($method['parameters']) || !is_array($method['parameters']) )
|
||||||
|
@ -379,16 +336,32 @@
|
||||||
if( isset($config['rename']) && is_string($config['rename']) && preg_match('@^\w+$@', $config['rename']) )
|
if( isset($config['rename']) && is_string($config['rename']) && preg_match('@^\w+$@', $config['rename']) )
|
||||||
$rename = $config['rename'];
|
$rename = $config['rename'];
|
||||||
|
|
||||||
/* (1) On récupère si le paramètre est optionnel ou pas */
|
/* (2) On récupère si le paramètre est optionnel ou pas */
|
||||||
$optional = isset($config['optional']) && $config['optional'] === true;
|
$optional = isset($config['optional']) && $config['optional'] === true;
|
||||||
|
|
||||||
/* (2) Si de type 'FILE' + fichier existe => on enregistre la ref. */
|
/* (3) Gestion du paramètre DEFAULT */
|
||||||
|
$default = null;
|
||||||
|
|
||||||
|
/* (3.1) Check if default NOT (NULL || FILE) -> matches TYPE */
|
||||||
|
if( isset($config['default']) ){
|
||||||
|
|
||||||
|
/* (3.1.1) Set default value from config */
|
||||||
|
$default = $config['default'];
|
||||||
|
|
||||||
|
/* (3.1.2) If FILE and not null -> Check type */
|
||||||
|
if( $config['type'] != 'FILE' || $default != null )
|
||||||
|
if( !Checker::run($config['type'], $default) )
|
||||||
|
return $this->error->set(Err::WrongDefaultParam, $rename, $config['type']);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (4) Si de type 'FILE' + fichier existe => on enregistre la ref. */
|
||||||
if( $config['type'] == 'FILE' && isset($_FILES[$name]) )
|
if( $config['type'] == 'FILE' && isset($_FILES[$name]) )
|
||||||
$this->params[$rename] = &$_FILES[$name];
|
$this->params[$rename] = &$_FILES[$name];
|
||||||
|
|
||||||
/* (3) Si param obligatoire et manquant -> erreur */
|
/* (4) Si param obligatoire et manquant -> erreur */
|
||||||
if( !isset($this->raw_params[$name]) && !$optional )
|
if( !isset($this->raw_params[$name]) && !$optional )
|
||||||
return $this->error->set(Err::MissingParam, $name);
|
return $this->error->set(Err::MissingParam, $rename);
|
||||||
|
|
||||||
|
|
||||||
/* (2.3) Gestion des valeurs
|
/* (2.3) Gestion des valeurs
|
||||||
|
@ -396,15 +369,15 @@
|
||||||
/* (1) Si le paramètre est optionnel et manquant */
|
/* (1) Si le paramètre est optionnel et manquant */
|
||||||
if( $optional && !isset($this->raw_params[$name]) ){
|
if( $optional && !isset($this->raw_params[$name]) ){
|
||||||
|
|
||||||
// On le crée le param optionnel avec la valeur NULL
|
// On le crée le param optionnel avec la valeur @default
|
||||||
$this->params[$rename] = null;
|
$this->params[$rename] = $default;
|
||||||
|
|
||||||
/* (2) Si le paramètre est renseigné (sauf FILE) */
|
/* (2) Si le paramètre est renseigné (sauf FILE) */
|
||||||
}elseif( $config['type'] != 'FILE'){
|
}elseif( $config['type'] != 'FILE' ){
|
||||||
|
|
||||||
// Si la verification est fausse, on retourne faux
|
// Si la verification est fausse, on retourne faux
|
||||||
if( !Checker::run($config['type'], $this->raw_params[$name]) )
|
if( !Checker::run($config['type'], $this->raw_params[$name]) )
|
||||||
return $this->error->set(Err::WrongParam, $name, $config['type']);
|
return $this->error->set(Err::WrongParam, $rename, $config['type']);
|
||||||
|
|
||||||
// Sinon, on ajoute aux params qu'on enverra à l'appel
|
// Sinon, on ajoute aux params qu'on enverra à l'appel
|
||||||
else
|
else
|
||||||
|
@ -423,7 +396,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (7) Ajout des options a partir de la configuration
|
/* (6) Ajout des options a partir de la configuration
|
||||||
*
|
*
|
||||||
* @return correct<bool> Retourne FAUS en cas d'erreur
|
* @return correct<bool> Retourne FAUS en cas d'erreur
|
||||||
*
|
*
|
||||||
|
@ -432,7 +405,7 @@
|
||||||
|
|
||||||
/* (1) On récupère les options de la méthode en cours
|
/* (1) On récupère les options de la méthode en cours
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
$method = $this->schema['raw'][$this->path['path']][$this->path['method']];
|
$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 */
|
/* (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']) )
|
if( !isset($method['options']) || !is_array($method['options']) )
|
||||||
|
@ -468,41 +441,45 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* (8) Execute le traitement associe et remplie la reponse
|
/* (7) Execute le traitement associe et remplie la reponse
|
||||||
*
|
*
|
||||||
* @return answer<Response> Retourne une reponse de type <Response> si tout s'est bien passe
|
* @return answer<Response> Retourne une reponse de type <Response> si tout s'est bien passe
|
||||||
*
|
*
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
public function dispatch(){
|
public function dispatch(){
|
||||||
|
|
||||||
/* (1) On verifie qu'aucune erreur n'a ete signalee
|
/* (1) Vérifications de niveau 0
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
if( $this->error->get() !== Err::Success ) // si il y a une erreur
|
/* (1) Si erreur -> on dispatch à la réponse */
|
||||||
return new Response($this->error); // on la passe a la reponse
|
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
|
/* (2) On essaie d'instancier le module
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
$instance = ModuleFactory::getModule($this->path['path']);
|
$instance = ModuleFactory::getModule($this->id['path']);
|
||||||
|
|
||||||
if( $instance instanceof Error ){
|
if( $instance instanceof Error ){
|
||||||
$this->error->set(Err::UncallableModule, $this->path['path']);
|
$this->error->set(Err::UncallableModule, $this->id['path']);
|
||||||
return new Response($this->error);
|
return new Response($this->error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* (3) On verifie que la methode est amorcable
|
/* (3) On verifie que la methode est amorcable
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
if( !is_callable([$instance, $this->path['method']]) ){
|
if( !is_callable([$instance, $this->id['method']]) ){
|
||||||
$this->error->set(Err::UncallableMethod, $this->path['method']);
|
$this->error->set(Err::UncallableMethod, $this->id['method']);
|
||||||
return new Response($this->error);
|
return new Response($this->error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* (4) On amorce la methode
|
/* (4) On amorce la methode
|
||||||
---------------------------------------------------------*/
|
---------------------------------------------------------*/
|
||||||
/* (1) On lance la fonction */
|
/* (1) On lance la fonction */
|
||||||
$returned = call_user_func( [$instance, $this->path['method']], $this->params );
|
$returned = call_user_func( [$instance, $this->id['method']], $this->params );
|
||||||
|
|
||||||
/* (2) On appelle le destructeur (si défini) */
|
/* (2) On appelle le destructeur (si défini) */
|
||||||
$instance = null;
|
$instance = null;
|
||||||
|
@ -527,7 +504,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* EXECUTE LE TRAITEMENT ASSOCIE ET RENVOIE UN FICHIER AVEC LE HEADER ET LE BODY SPECIFIE
|
/* (8) Gestion d'un téléchargement HTTP
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function download(){
|
public function download(){
|
||||||
|
@ -615,6 +592,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* (9) Getter générique
|
||||||
|
*
|
||||||
|
* @index<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
Loading…
Reference in New Issue