diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d63cd90 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.gtm/ diff --git a/README.md b/README.md index 952c446..64a9b01 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ xdrm-framework is based on `all in config` so you will have this structure : > version 2.0 - [documentation](/notice/api/2.0.md) > version 2.2 - [documentation](/notice/api/2.2.md) +> version 3.0 - [documentation](/notice/api/3.0.md) #### 3.2 Log - multilog system diff --git a/exporter/packages.json b/exporter/packages.json index 781f18e..16a3172 100755 --- a/exporter/packages.json +++ b/exporter/packages.json @@ -38,6 +38,14 @@ "http": [ "1.0" ] + }, + "3.0": { + "error": [ + "2.0" + ], + "http": [ + "1.0" + ] } }, "db-schema": { @@ -69,19 +77,30 @@ "error": [ "2.0" ] + }, + "3.0": { + "error": [ + "2.0" + ] } }, + }, "lightdb": { "1.0": [] }, "router": { "1.0": [], "2.0": [] + }, + "token": { + "0.9": [] } }, "installed": { - "db-schema": "1.0", - "database": "2.0", - "error": "2.0" + "api": "2.2", + "error": "2.0", + "http": "1.0", + "router": "2.0", + "database": "2.0" } } \ No newline at end of file diff --git a/notice/api/3.0.md b/notice/api/3.0.md new file mode 100644 index 0000000..19e1052 --- /dev/null +++ b/notice/api/3.0.md @@ -0,0 +1,454 @@ +```yaml +module: api +version: 3.0 +requires: + - http: 1.0 + - error: 2.0 +``` + +Links +==== + +[**I.** Overview](#i-overview) + +- [**1** Introduction & features](#1-introduction--features) +- [**2** Basic knowledge](#2-basic-knowledge) + +[**II.** Usage](#ii-usage) + +- [**1** Setup](#1-setup) +- [**2** Php requirements](#2-php-requirements) +- [**3** From php internally](#3-from-php-internally) +- [**4** From HTTP requests](#4-from-http-requests) + +[**III.** Configuration](#iii-configuration) + +- [**1** Basic usage](#1-basic-usage) +- [**2** Advanced usage](#2-advanced-usage) + +[**IV.** Implementation](#iv-implementation) + +- [**1** Permissions : AuthSystem](#1-permissions--authsystem) +- [**2** Modules & methods](#2-modules--methods) +- [**3** Automatic type check](#3-automatic-type-check) + +[**V.** Class documentation](#v-class-documentation) + +- [**1** Request](#1-request) +- [**2** Response](#2-response) +- [**3** AuthSystem](#4-authsystem) +- [**4** Checker](#4-checker) +- [**5** ModuleFactory](#4-modulefactory) + + + +> # **I.** Overview + +> ## **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 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. + +Things you have to do : +- implement your processes (obviously) +- implement your authentication system (cf. [AuthSystem](#1-permissions--authsystem)) +- edit the configuration file (cf. [configuration](#iii-configuration)) + +Things you **don't have** to do : +- input type check (cf. [Checker](#4-checker)) +- API multiple permission management +- optional or required inputs +- before and after scripts +- catch both in-URL and `multipart/form-data` input + +> ## **2** Basic knowledge + +The API is based over a 2-level delegation structure : +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`. + +Example: +* the module `article` contains methods: + * `read` with argument `article_id` (to identify the wanted article) + * `write` with arguments `title` and `body` (data to write into the new created article) + * `edit` with arguments `article_id` and `body` (to identify and replace the body) + * `delete` with argument `article_id` (to identify which article to delete) + +If you want to delete the article of id `52`, you must request `article/delete` passing `article_id`=`52`. + + + + + + + + + +> # **II.** Usage + +> ## **1** Setup + +In order to make the API work, you have to : +1. Edit the configuration file according to your needs (cf. [configuration](#iii-configuration)) +2. Implement the Authentication System to manage permissions (cf. [AuthSystem](#1-permissions--authsystem)) +3. Implement the code of the methods according to the configuration + + +> ## **2** Php requirements + +> ### 1) include the `autoloader` file + +```php + ### 2) load useful classes + +```php +// for API use +use \api\core\Request; +use \api\core\Response; + +// for error handling +use \error\core\Err; +``` + + + + +> ## **3** From php internally + + +> ### 1) create a request + +```php +// creates a request for the module {module} and its method {method} with params +$request = new Request('{module}/{method}', [ + 'param1' => 10, + 'param2' => 'somevalue' +]); +``` + +> ### 2) catch possible errors (optional) + +```php +// if error is not Err::Success +if( $request->error->get() !== Err::Success ) + 'do something'; +``` + +> ### 3) execute the request and catch response + +```php +$response = $request->dispatch(); +``` + +> ### 4) catch response errors (optional) + +```php +// if error is not Err::Success +if( $response->error->get() !== Err::Success ) + 'do something'; +``` + +> ### 5) catch response output + +```php +// fetch all outputs +$output = $response->getAll(); + +// fetch specific output +$specific = $response->get('someOutputName'); + ``` + + +> ## **4** From HTTP requests +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}` + +```php +// let's suppose the url is `/api/{module}/{method}` +$url = '/api/somemodule/somemethod/1/2/'; +$uri = substr($url, strlen('/api')); +// $uri = /somemodule/somemethod/1/2/ +``` + +> ### 2) give the url to the HTTP manager + +```php +// create request from HTTP data +$request = Request::remote($url); + +// execute request and catch response +// note that request errors will propagate through response +$response = $request->dispatch(); + +// return response as HTTP body +die( $response->serialize() ); +``` + + + +Then can handle various kinds of URL : + +- request and parameters can be in URL (separated by `/`) +- request and parameters can be in `multipart/form-data` or `x-www-form-urlencoded` +- request and parameters of both URL, post data, and form-data are caught + +### The following examples can work : +> 1. `http://www.host.com/{module}/{method}/` +```json +"post-data": { + "param1": "{value1}", + "param2": "{value2}" +} +``` +> 2. `http://www.host.com/{module}/{method}/{param1}/{param2}` +```json +"post-data": {} +``` +> 3. `http://www.host.com/apiOrParentUrl/` +```json +"post-data": { + "module": "{module}", + "method": "{method}", + "param1": "{value1}", + "param2": "{value2}" +} +``` +> 4. `http://www.host.com/apiOrParentUrl/{value1}/{value2}` +```json +"post-data": { + "module": "{module}", + "method": "{method}", +} +``` + + + + + + +> # **III.** Configuration + +```json +{ + + "{module_name}": { + + "{http_method}::{method_name}": { + "description": "{method_description}", + "permissions": ["{method_perm}"], + "options": { "download": "{is_downloadable}" }, + "parameters": { + "{name_param}": { "description": "{desc_param}", "type": "{type_param}", "optional": "{is_optional}" } + }, + "output": { + "{name_output}": { "description": "{desc_output}", "type": "{type_output}" } + } + } + + } +} +``` + +|variable|description|exemple| +|-------|-------|------| +|`{module_name}`|alphanumeric module name|"publications"| +|`{http_method}`|uppercase HTTP method|"POST"| +|`{method_name}`|alphanumeric method name|"article"| +|`{method_description}`|textual description|"Returns a specific article"| +|`{method_perm}`|permission array|`["poster", "admin", "user"]`| +|`{is_downloadable}`|If you want this method to return a file|`true`, `false`| +|`{name_param}`|Your param's name _*_|"id_article"| +|`{desc_param}`|Your param's description|"Wanted article's id"| +|`{type_param}`|Your param's type (cf. Checker)|"Wanted article's type"| +|`{is_optional}`|Whether to make your param _required_|`true`, `false`| +|`{name_output}`|Your output's name|"article"| +|`{desc_output}`|Your output's description|"Article content"| + +_*_ If you want URL (GET) parameters, the {param_name} must be `URL_0`, `URL_1` and so on according to the index wanted in the URL. +> `api/module/method/URL_0/URL_1/URL_2/` + + + + + + + +> # **IV.** Implementation + +> ## **1** Permissions : AuthSystem + +In order to implement your _Authentification System_ you have to implement the **interface** `AuthSystem` located in `/build/api/core/AuthSystem`. + +You must register your custom authentification system before each api call with : +```php +// let's suppose your auth system class is "AuthSystemDefault" +\api\core\Request::setAuthSystem(new AuthSystemDefault); +``` + +> ## **2** Modules & methods + +### Module implementation +Each module's implementation is represented as a **file** so as a **class** located in `/build/api/module/`. In order for the autoloader to work, you must name the **file** the same name as the **class**. + +### Method implementation +Each method is represented as a method in its module's class. + +### Input arguments + +Arguments are passed to the method as a single argument which an associative array according to the documentation. + +_Notes_: +* Optional parameters if not given are set to `null` +* parameters of type `FILE` are given by reference but the use is the same as normal parameters +* URL parameters are called `URL_0`, `URL_1` and so on according to their order. + +### Ouput required + +You must return an associative array containing at least the field `error` containing an instance of `/api/core/Error`, then you can add whatever you want to return in the array. + +If you don't return the 'error' field, by default it is set to `new Error(Err::Success)`. + + + + +> # **V.** Class documentation + +> ## **1** Request + +### Attributes + +The attribute `error` will contain the current `Error` instance. + +```php + ## **2** Response + +### Attributes + +The attribute `error` will contain the current `Error` instance. + +```php + ## **4** Checker + +`Checker` checks the input values according to the type given in the configuration. + +The default types below are available in the default package. +To add a new type, just open the file `/build/api/Checker.php` and add an entry in the `switch` statement. + +### Default types +|Type|Example|Description| +|---|---|---| +|`mixed`|`[9,"a"]`, `"a"`|Any content (can be simple or complex)| +|`id`|`10`, `"23"`|Positive integer number between `0` and `2147483647`| +|`numeric`|`-10.2`, `"23"`|Any number, `null` and the string `"null"`| +|`text`|`"Hello!"`|String that can be of any length (even empty)| +|`hash`|`"4612473aa81f93a878674f9ebffa8d63a1b51ea28dcdcdb1e89eb512aae9b77e"`|String with a length of 40 or 64, containing only hexadecimal characters| +|`alphanumeric`|`"abc029.-sd9"`|String containing only alphanumeric, ___, _-_, and _._ characters| +|`letters`|`"abc -sd"`|String containing only letters, _-_, and space characters| +|`mail`|`"a.b@c.def"`|Valid email address| +|`number`|`0102030405`|Phone number, following formats allowed : `06`, `+336`, `+33 6`| +|`array`|`[1, 3]`|Non-empty array| +|`object`|_works only within php_|Non-empty object| +|`boolean`|`true`, `false`|Boolean| +|`varchar(a,b)`|`"Hello!"`|String with a length between `a` and `b` (included)| +|`varchar(a,b,c)`|`"abc"`|String with a length between `a` and `b` (included) and matching the `c` type| + +### Complex type : chainable array + +|Type|Sub-Type|Description| +|---|---|---| +|`array`|`a`|Array containing only entries matching the type `a`| + +> **Note:** It is possible to chain `array` type as many as needed. +**Ex.:** `array>` - Will match array only containing arrays that only contains `id` entries. + + +> ## **5** Advanced + +### Before and After scripts + +Each time a **method** is called, the api **creates an instance** from the class, and after the execution, the class is **destroyed**. So you can implement the methods `__construct` and `__destruct` to add before and after scripts. \ No newline at end of file diff --git a/notice/orm/0.8-2.md b/notice/orm/0.8-2.md index fc83882..fd4affa 100644 --- a/notice/orm/0.8-2.md +++ b/notice/orm/0.8-2.md @@ -9,12 +9,12 @@ Overview ---- > ### 1. Introduction -The `orm` package allows you to simplify SQL requests through php objects. You will never have to use SQL code anymore. +The `orm` package allows you to simplify SQL requests through php chain-methods. You will never have to use SQL code anymore. > ### 2. Setup -In order for the orm to work, you will ne to configure the `database` driver according to your database credentials and information. +In order for the orm to work, you will have to configure the `database` driver according to your database credentials and information. > ### 3. Features @@ -49,7 +49,7 @@ Specification Usage ---- -> ### (1) Loader +> #### [1] Loader ```php ### (2) SELECT queries +> #### [2] SELECT queries -> #### (2.1) Single Table +> ##### (2.1) Single Table ```php #### (2.3) Select +> ##### (2.3) Select ```php fetch(); ``` -> #### (2.4) Order by +> ##### (2.4) Order by ```php whereUsername('someusername') // if username is equal to 'someusername' ->whereMail(['somemail', Rows::COND_EQUAL]) // same as previous line (explicit here) ->fetch(); +``` + +Request examples +---- + +> ##### `1. Select all` + +```sql +SELECT * +FROM user_table +``` + +```php +select('*') + ->fetch(); +``` +> ##### `2. Normal select` + +```sql +SELECT id_user, username, mail +FROM user_table +``` + +```php +select('id_user') + ->select('username') + ->select('mail') + ->fetch(); +``` +> ##### `3. Select distinct` + +```sql +SELECT DISTINCT id_user, -- distinct + username, + mail +FROM user_table +``` + +```php +select('id_user', null, true) // 2nd arg is for aggregation functions + ->select('username') + ->select('mail') + ->fetch(); +``` + + +> ##### `4. Aggregation functions` + +```sql +SELECT id_post, + count(nb_view) +FROM posts +``` + +```php +select('id_post') + ->select('nb_view', Rows::SEL_COUNT) + ->fetch(); +``` + + +> ##### `5. Select as (alias)` + +```sql +SELECT id_post, + count(nb_view) as NB_COUNT +FROM posts +``` + +```php +select('id_post') + ->select('nb_view', Rows::SEL_COUNT, null, 'NB_COUNT') + ->fetch(); +``` + + +> ##### `6. Single PRIMARY KEY condition` + +```sql +SELECT * +FROM posts +WHERE id_post = 10 +``` + +```php +select('*') + ->whereId(10) + ->fetch(); +``` + +> ##### `7. Composite PRIMARY KEY condition` + +```sql +SELECT * +FROM posts +WHERE id_post = 10 -- primary key (field 1) +AND id_user = 11 -- primary key (field 2) +``` + +```php +select('*') + ->whereId([10, 11]) + ->fetch(); +``` + + +> ##### `8. Condition types` + +```sql +SELECT * +FROM complex_table +WHERE a = 10 +AND b <> 11 +AND c < 12 +AND d > 13 +AND e <= 14 +AND f >= 15 +AND g LIKE '%16%' +AND h IN (2, 4, 6, 8) +``` + +```php +select('*') + ->whereA( 10 ) + ->whereB( [11, Rows::COND_NOTEQ] ) + ->whereC( [12, Rows::COND_INF] ) + ->whereD( [13, Rows::COND_SUP] ) + ->whereE( [14, Rows::COND_INFEQ] ) + ->whereF( [15, Rows::COND_SUPEQ] ) + ->whereG( ['%16%', Rows::COND_LIKE] ) + ->whereH( [[2, 4, 6, 8], Rows::COND_IN] ) + ->fetch(); ``` \ No newline at end of file diff --git a/notice/token/0.9.md b/notice/token/0.9.md new file mode 100644 index 0000000..781c32f --- /dev/null +++ b/notice/token/0.9.md @@ -0,0 +1,309 @@ +```yaml +module: token +version: 0.9 +``` + +Overview +---- + +> ### 1. Introduction +The `token` package features the `TreeToken`. It allows securing PHP sessions and children instances. + +> ### 3. Features + +PHP SESSION + +- Prevent XSS (_PHPSESSID_ theft) +- Each PHP load has a single-use token + - that checks that last call was yours + - that unsets the session if someone theft your _PHPSESSID_ +- Manage `INSERT INTO` queries +- Manage `DELETE` queries + +Specification + +- Fetches the whole schema specification (foreign keys, primary keys, etc) +- Manage `SELECT *` +- Manage composite `PRIMARY KEY` +- Manage `WHERE` conditions (_=_, _<>_, _<_, _>_, _>=_, _<=_, _LIKE_, _IN_) +- Manage aggregation functions (_AVG()_, _SUM()_, _MAX()_, _MIN()_, _COUNT()_, *GROUP_CONCAT()*) +- Manage `ORDER BY` ordering +- Manage `SELECT DISTINCT` specification (_ASC_, _DESC_) +- Inserting multiple rows at once +- Automatically select the **PRIMARY KEY(S)** +- Manage joined tables +- Manage `fetch` and `fetchAll` +- Manage `NULL` keyword +- Manage booleans +- Manage inserting the `DEFAULT` value +- Manage format beautifying (numbers as numbers, same for booleans, null) + + + + +Usage +---- + +> #### [1] Loader + +```php + #### [2] SELECT queries + +> ##### (2.1) Single Table + +```php +select('*') + ->fetch(); + + // First row only + Table::get('table_name') + ->select('*') + ->unique() + ->fetch(); + +``` + +> ##### (2.3) Select + +```php +select('field_1') + ->select('field_2') + /// ... + ->select('field_N') + ->fetch(); +``` + +> ##### (2.4) Order by + +```php +orderby('field_name', Rows::ORDER_ASC) + ->fetch(); + + // Descending order of the field `field_name` + Table::get('table_name') + ->orderby('field_name', Rows::ORDER_DESC) + ->fetch(); + +``` + +> #### (2.5) WhereId + +It will match the corresponding `PRIMARY KEY` of the table, if it is a composed key (multiple fields) instead of giving an argument, give an array for each in the order displayed in _phpmyadmin_ or you mysql viewer. + +```php + `id_user` + Table::get('user') + ->select('*') // select all fields + ->whereId(12) // if id_user is equal to 12 + ->fetch(); // fetch matching rows + + // PRIMARY KEYS => `username` + `mail` + Table::get('user') + ->select('*') // select all fields + ->whereId([12, 'sample@mail.com']) + // if `id_user` is equal to 12 + // AND `mail` is equal to 'sample@mail.com' + ->fetch(); // fetch matching rows +``` + +The available condition operators are listed in the [constants](todo) section. +Note: `Rows::COND_EQUAL` is set by default if missing + +> #### (2.6) Where clause + +The where clause uses one of php's magic functions (\_\_call). So the name of the method you call will contain the field of the condition. But you must use the correct case, removing '\_' and setting the next character to upper case. The rest will be forced to lower case. + +You can refer to the following examples: + + +|Field|Method name| +|---|---| +|username|`whereUsername`| +|id_user|`whereIdUser`| +|aaa_bb_c_ddd|`whereAaaBbCDdd`| + +```php +select('*') + ->whereUsername('someusername') // if username is equal to 'someusername' + ->whereMail(['somemail', Rows::COND_EQUAL]) // same as previous line (explicit here) + ->fetch(); +``` + +Request examples +---- + +> ##### `1. Select all` + +```sql +SELECT * +FROM user_table +``` + +```php +select('*') + ->fetch(); +``` +> ##### `2. Normal select` + +```sql +SELECT id_user, username, mail +FROM user_table +``` + +```php +select('id_user') + ->select('username') + ->select('mail') + ->fetch(); +``` +> ##### `3. Select distinct` + +```sql +SELECT DISTINCT id_user, -- distinct + username, + mail +FROM user_table +``` + +```php +select('id_user', null, true) // 2nd arg is for aggregation functions + ->select('username') + ->select('mail') + ->fetch(); +``` + + +> ##### `4. Aggregation functions` + +```sql +SELECT id_post, + count(nb_view) +FROM posts +``` + +```php +select('id_post') + ->select('nb_view', Rows::SEL_COUNT) + ->fetch(); +``` + + +> ##### `5. Select as (alias)` + +```sql +SELECT id_post, + count(nb_view) as NB_COUNT +FROM posts +``` + +```php +select('id_post') + ->select('nb_view', Rows::SEL_COUNT, null, 'NB_COUNT') + ->fetch(); +``` + + +> ##### `6. Single PRIMARY KEY condition` + +```sql +SELECT * +FROM posts +WHERE id_post = 10 +``` + +```php +select('*') + ->whereId(10) + ->fetch(); +``` + +> ##### `7. Composite PRIMARY KEY condition` + +```sql +SELECT * +FROM posts +WHERE id_post = 10 -- primary key (field 1) +AND id_user = 11 -- primary key (field 2) +``` + +```php +select('*') + ->whereId([10, 11]) + ->fetch(); +``` + + +> ##### `8. Condition types` + +```sql +SELECT * +FROM complex_table +WHERE a = 10 +AND b <> 11 +AND c < 12 +AND d > 13 +AND e <= 14 +AND f >= 15 +AND g LIKE '%16%' +AND h IN (2, 4, 6, 8) +``` + +```php +select('*') + ->whereA( 10 ) + ->whereB( [11, Rows::COND_NOTEQ] ) + ->whereC( [12, Rows::COND_INF] ) + ->whereD( [13, Rows::COND_SUP] ) + ->whereE( [14, Rows::COND_INFEQ] ) + ->whereF( [15, Rows::COND_SUPEQ] ) + ->whereG( ['%16%', Rows::COND_LIKE] ) + ->whereH( [[2, 4, 6, 8], Rows::COND_IN] ) + ->fetch(); +``` \ No newline at end of file diff --git a/notice/token/child-normal.svg b/notice/token/child-normal.svg new file mode 100644 index 0000000..510e470 --- /dev/null +++ b/notice/token/child-normal.svg @@ -0,0 +1,747 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PHPSESSID@A + REQUEST + + + + token@1 + RESPONSE + + + + PHPSESSID@A + token@1 + REQUEST + + + + token@2 + RESPONSE + + + session@1 + + + + + PHPSESSID: okTOKEN: init + + ... + + + + + + + + + + + A + + + + PHPSESSID: okTOKEN: ok + session@1 + + + + + + + + + + + + + + + + + + + + + + + + + PHPSESSID@A + token@2 + REQUEST + + + + token@2.1 + RESPONSE + + + PHPSESSID: okTOKEN: okSUBTOKEN: ok + session@1 + + + + + + + + PHPSESSID@A + token@2.1 + REQUEST + + + + token@2.2 + RESPONSE + + + PHPSESSID: okTOKEN: okSUBTOKEN: ok + session@1 + + + + + + ... + + + diff --git a/notice/token/parent-normal.svg b/notice/token/parent-normal.svg new file mode 100644 index 0000000..eca427a --- /dev/null +++ b/notice/token/parent-normal.svg @@ -0,0 +1,473 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PHPSESSID@A + REQUEST + + + + token@1 + RESPONSE + + + + PHPSESSID@A + token@1 + REQUEST + + + + token@2 + RESPONSE + + + session@1 + + + + + PHPSESSID: okTOKEN: init + + ... + + + + + + + + + + + A + + + + PHPSESSID: okTOKEN: ok + session@1 + + + + + + + + diff --git a/notice/token/parent-xss-no-token.svg b/notice/token/parent-xss-no-token.svg new file mode 100644 index 0000000..065b164 --- /dev/null +++ b/notice/token/parent-xss-no-token.svg @@ -0,0 +1,756 @@ + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PHPSESSID@A + REQUEST + + + + token@1 + RESPONSE + + + + PHPSESSID@A + token@1 + REQUEST + + + + token@2 + RESPONSE + + + session@1 + + + + + PHPSESSID: okTOKEN: init + + ... + + + + + + + + + + + A + + + + + + + + + + + B + + + + + + stole PHPSESSID@A + + + + PHPSESSID@A + REQUEST + + + + PHPSESSID@A + + + PHPSESSID: okTOKEN: ok + session@1 + + + + + + + + + + + session@2 + PHPSESSID: okTOKEN: missing + + + + PHPSESSID@A + token@2 + REQUEST + + + + token@3 + RESPONSE + + + PHPSESSID: unsetTOKEN: ok + session@3 + + + + + + + + diff --git a/notice/token/parent-xss.svg b/notice/token/parent-xss.svg new file mode 100644 index 0000000..520bc3e --- /dev/null +++ b/notice/token/parent-xss.svg @@ -0,0 +1,895 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PHPSESSID@A + REQUEST + + + + token@1 + RESPONSE + + + + PHPSESSID@A + token@1 + REQUEST + + + + token@2 + RESPONSE + + + session@1 + + + + + PHPSESSID: okTOKEN: init + + ... + + + + + + + + + + + A + + + + + + + + + + + B + + + + + + stole PHPSESSID@A + token@2 + + + + PHPSESSID@A + token@2 + REQUEST + + + + PHPSESSID@A + + + PHPSESSID: okTOKEN: ok + session@1 + + + + + + + session@1 + PHPSESSID: okTOKEN: ok + + + + + + + + PHPSESSID@A + token@2 + REQUEST + + + + token@3 + RESPONSE + + + PHPSESSID: okTOKEN: error + session@2 + + + + + + + + + token@3 + REQUEST + + + + PHPSESSID@A + + + PHPSESSID: unsetTOKEN: ok + session@3 + + + + + + + + token@3 + RESPONSE + + + + diff --git a/src/packages/api/2.2/core/Request.php b/src/packages/api/2.2/core/Request.php index 8db689c..a543046 100644 --- a/src/packages/api/2.2/core/Request.php +++ b/src/packages/api/2.2/core/Request.php @@ -1,5 +1,14 @@ Liste des permissions attendues + * + * @return error Erreur associée à la permission (Success/PermissionError/TokenError/etc) + * + */ + public static function permission($expected); + } + +?> diff --git a/src/packages/api/3.0/core/Checker.php b/src/packages/api/3.0/core/Checker.php new file mode 100755 index 0000000..02d631b --- /dev/null +++ b/src/packages/api/3.0/core/Checker.php @@ -0,0 +1,159 @@ + Type que l'on veut verifier + * @value Valeur a verifier + * + * @return match Retourne si oui ou non la valeur @value est du bon type @type + * + */ + public static function run($type, $value){ + $checker = true; + + /* [0] On verifie que $value n'est pas nul + =========================================================*/ + if( is_null($value) ) return false; + + + + /* [1] Si de type VARCHAR(min, max, flags) + =========================================================*/ + if( preg_match('/^varchar\((\d+), ?(\d+)((?:, ?\w+)+)?\)$/', $type, $match) ){ + // On recupere la taille min + $min = (int) $match[1]; + // On recupere la taille max + $max = (int) $match[2]; + + // On recupere le sous-type si défini + $flags = isset($match[3]) ? explode(',', substr($match[3], 1)) : null; + + // On effectue la verification de taille + $lenCheck = $checker && is_string($value) && strlen($value) <= $max && strlen($value) >= $min; + + // On vérifie les FLAGS s'il est donné + if( is_array($flags) ) + foreach( $flags as $flag ) + $lenCheck = $lenCheck && self::run($flag, $value); + + return $lenCheck; + } + + + /* [2] Si de type ARRAY(type_elements) + =========================================================*/ + if( preg_match('/^array<(.+)>$/', $type, $match) ){ + + // Si c'est pas un tableau on retourne une erreur + if( !is_array($value) ) + return false; + + + $elements_type = $match[1]; + + // On verifie le type pour chaque element + foreach($value as $element) + // Si erreur dans au moins 1 element, on retourne que c'est incorrect + if( !self::run($elements_type, trim($element) ) ) + return false; + + // Si aucune erreur, on retourne que tout est bon + return true; + } + + + /* [n] Sinon, tous les autres types definis + =========================================================*/ + switch($type){ + // Quoi que ce soit + case 'mixed': + return $checker && !is_null($value); + break; + + // Entier positif (id dans BDD) + case 'id': + return $checker && is_numeric($value) && $value <= 2147483647 && $value >= 0; + break; + + // String quelconque (peut etre vide) + case 'text': + return $checker && is_string($value); + + // Adresse mail (255 caracteres max) + case 'mail': + return $checker && is_string($value) && strlen($value) <= 50 && preg_match('/^[\w\.-]+@[\w\.-]+\.[a-z]{2,4}$/i', $value); + break; + + // Hash sha1/md5 + case 'hash': + return $checker && is_string($value) && preg_match('/^[\da-f]{128}$/', $value); + break; + + case 'alphanumeric': + return $checker && is_string($value) && preg_match('/^[\w\.-]+$/ui', $value); + break; + + case 'letters': + return $checker && is_string($value) && preg_match('/^[a-z -]+$/i', $value); + break; + + case 'status': + return $checker && is_numeric($value) && floor($value) == $value && $value >= 0 && $value <= 100; + break; + + // Tableau non vide + case 'array': + return $checker && is_array($value) && count($value) > 0; + break; + + // Boolean + case 'boolean': + return $checker && is_bool($value); + break; + + // Objet non vide + case 'object': + return $checker && is_object($value) && count((array) $value) > 0; + break; + + // Chaine JSON (on vérifie via le parser) + case 'json': + return $checker && is_string($value) && json_decode($value, true) !== NULL; + break; + + case 'numeric': + return $checker && (is_numeric($value) || $value == null || $value == 'null'); + break; + + case "float": + return $checker && is_float($value); + break; + + default: + return false; + break; + } + + return $checker; + + } + + + } +?> diff --git a/src/packages/api/3.0/core/Loader.php b/src/packages/api/3.0/core/Loader.php new file mode 100755 index 0000000..df9da92 --- /dev/null +++ b/src/packages/api/3.0/core/Loader.php @@ -0,0 +1,53 @@ + URI + * + * @return outName outDesc + * + ---------------------------------------------------------*/ + public static function remote($uri){ + + /* (1) Fetch HttpRequest correct data + ---------------------------------------------------------*/ + /* (1) Parse HttpRequest data because php doesn't parse it for non-POST HTTP method */ + $httprequest = new HttpRequest(); + + /* (2) For later use -> replace default @_POST global */ + $_POST = $httprequest->POST(); + + /* (3) Get @data from @_POST values */ + $data = $_POST; + + + /* (2) Build request + ---------------------------------------------------------*/ + return new Request($uri, $data); + } + + + + + } \ No newline at end of file diff --git a/src/packages/api/3.0/core/ModuleFactory.php b/src/packages/api/3.0/core/ModuleFactory.php new file mode 100755 index 0000000..1c9109d --- /dev/null +++ b/src/packages/api/3.0/core/ModuleFactory.php @@ -0,0 +1,45 @@ + Nom du module + * @arguments [OPTIONNEL] Arguments à passer au constructeur + * + * @return instance Instance du module en question + * + */ + public static function getModule($module, $arguments=[]){ + /* (1) On gère les arguments */ + $arguments = is_array($arguments) ? $arguments : []; + + /* (2) On transforme @module en namespace */ + $module_ns = str_replace('/', '\\', $module); + + + /* (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_ns"; + + /* (3) On retourne une instance */ + return new $class_name($arguments); + } + + } diff --git a/src/packages/api/3.0/core/Request.php b/src/packages/api/3.0/core/Request.php new file mode 100755 index 0000000..305861b --- /dev/null +++ b/src/packages/api/3.0/core/Request.php @@ -0,0 +1,620 @@ + false ]; + private static $authsystem = null; + // liste des methodes HTTP autorisées + private static $allowed_http_methods = [ "GET", "POST", "PUT", "DELETE" ]; + + + // Attributs prives utiles (initialisation) + private $path; // chemin de base (uri) + private $raw_params; // paramètres reçus + private $params; // paramètres donnés à la fonction + private $schema; // schema configuration + private $options; // options + private $http_method; // methode HTTP appelante + + // Contiendra la reponse a la requete + public $answer; + + // Contiendra l'etat de la requete + public $error; + + + /* (0) Constructeur d'une requete de module + * + * @uri URI relative de l'appel + * @param Tableau associatif contenant les parametres utiles au traitement + * @forced_method Méthode demandée (optionnel) + * + * @return instance Instance crée + * + ---------------------------------------------------------*/ + public function __construct($uri=null, $params=null, $forced_method=null){ + + return $this->buildRequestObject($uri, $params, $forced_method); + + } + + + + /* (1) Constructeur d'une requete de module (delegation) + * + * @uri URI relative de l'appel + * @param Tableau associatif contenant les parametres utiles au traitement + * @forced_method Méthode demandée (optionnel) + * + * @return status Retourne si oui ou non tout s'est bien passe + * + ---------------------------------------------------------*/ + private function buildRequestObject($uri=null, $params=null, $forced_method=null){ + + /* (1) Initialisation + ---------------------------------------------------------*/ + /* (1) Erreur par défaut */ + $this->error = new Error(Err::Success); + + /* (2) Si pas parametre manquant, on quitte */ + if( $uri == null ) + return $this->error->set(Err::MissingPath); + + + /* (2) On met a jour la configuration + ---------------------------------------------------------*/ + /* (1) Build from configuration */ + $this->buildConfig(); + + /* (2) Dispatch if error */ + if( $this->error->get() != Err::Success ) + return; + + + /* (3) Verification des types des parametres + ---------------------------------------------------------*/ + /* (1) Si path est une */ + if( !is_string($uri) ) // Si le type est incorrect + return $this->error->set(Err::WrongPathModule); + + /* (2) Formattage @params en tableau */ + $this->raw_params = (is_array($params)) ? $params : []; + + /* (3) On définit en constante la méthode HTTP */ + if( !isset($_SERVER['REQUEST_METHOD']) && !is_string($forced_method) ) + return $this->error->set(Err::UnknownHttpMethod); + + $this->http_method = is_string($forced_method) ? strtoupper($forced_method) : strtoupper($_SERVER['REQUEST_METHOD']); + + + /* (4) Verification du chemin (existence module+methode) + ---------------------------------------------------------*/ + if( !$this->checkURI($uri) ) // Verification de la coherence du chemin + attribution + return false; // checkURI() sets the error itself + + + /* (5) Verification des permissions + ---------------------------------------------------------*/ + 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() ) // 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['HTTP_METHOD'] = $this->http_method; + $this->error->set(Err::Success); + + return true; // On retourne que tout s'est bien passe + } + + + + /* (2) Definit le systeme d'authentification + * + * @instance Instance de type AuthSystem + * + * @return success Whether the AuthSystem is valid or not + * + ---------------------------------------------------------*/ + public static function setAuthSystem($instance=null){ + /* (1) Check instance type */ + if( !($instance instanceof AuthSystem) ) + return false; + + /* (2) Store instance */ + self::$authsystem = $instance; + + return true; + } + + + + /* (3) Construction du schéma à partir de la configuration + * + ---------------------------------------------------------*/ + 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 correspondant au chemin de delegation ("module/methode") + * + * @return validity Retourne si oui ou non l'objet est correct + * + ---------------------------------------------------------*/ + private function checkURI($uri){ + + /* (1) Verification format general + ---------------------------------------------------------*/ + /* (1) If wrong format -> exit */ + if( !preg_match('@^\w+(\/\w+)*\/?$@', $uri, $matches) ) + return $this->error->set(Err::WrongPathModule); + + + /* (2) Verification de l'existence du chemin (conf) + ---------------------------------------------------------*/ + /* (1) Check if begins with each indexed @path */ + $exists_size = 0; + $path = null; + + foreach($this->schema['index'] as $key=>$void){ + $match_size = strlen($key); + + /* (1.1) Look for the longer match */ + if( $match_size > $exists_size && substr($uri, 0, $match_size) == $key ){ + $exists_size = $match_size; + $path = $key; + } + + } + + /* (2) If @path not found -> exit */ + if( is_null($path) ) + return $this->error->set(Err::UnknownModule); + + + /* (3) Extract URI parameters + ---------------------------------------------------------*/ + /* (1) Extract URI string after @path */ + $uri_end = substr($uri, $exists_size); + + /* (2) If invalid format, return error */ + if( !preg_match('@^((?:\/[^\/]+)*)\/?$@', $uri_end, $uri_match) ) + return $this->error->set(Err::InvalidURI); + + /* (3) Add each URI parameter to the parameter store */ + $uri_args = array_slice( explode('/', $uri_match[1]), 1); + + foreach($uri_args as $index=>$value) + $this->raw_params["URL$index"] = $value; + + /* (4) Verification de l'existence de la methode (conf) + ---------------------------------------------------------*/ + /* (1) Check if HTTP method is in allowed methods */ + if( !in_array($this->http_method, self::$allowed_http_methods) ) + return $this->error->set(Err::UnknownHttpMethod, $this->http_method); + + /* (2) Check if HTTP method is defined for this @path */ + if( !in_array($this->http_method, $this->schema['index'][$path]) ) + return $this->error->set(Err::UnknownMethod, $this->http_method); + + + + /* (5) Enregistrement du chemin et renvoi de SUCCESS + ---------------------------------------------------------*/ + $this->path = [ + 'path'=> $path, + 'method'=> $this->http_method + ]; + + return true; + } + + + + /* (5) 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->schema['raw'][$this->path['path']][$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( $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; + + } + + + + /* (6) Verification du type des parametres envoyes + * + * @return correct Retourne si oui ou non les parametres ont le bon type + * + ---------------------------------------------------------*/ + private function checkParams(){ + + /* (1) On verifie qu'il ne manque aucun parametre + ---------------------------------------------------------*/ + /* (1) Si @params n'est pas un tableau */ + if( !is_array($this->raw_params) ) + return $this->error->set(Err::MissingParam); + + /* (2) On récupère les données de la méthode */ + $method = $this->schema['raw'][$this->path['path']][$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=>$config){ + + /* (2.1) Vérification des données + ---------------------------------------------------------*/ + /* (1) Si @name n'est pas une string */ + if( !is_string($name) ) + return $this->error->set(Err::ConfigError); + + /* (2) Si @config n'est pas un tableau */ + if( !is_array($config) ) + return $this->error->set(Err::ConfigError); + + /* (3) So @config['type] manquant ou incorrect */ + if( !isset($config['type']) || !is_string($config['type']) ) + return $this->error->set(Err::ConfigError); + + + /* (2.2) Gestion des spécifications + ---------------------------------------------------------*/ + /* (1) On récupère le paramètre RENAME */ + $rename = $name; + if( isset($config['rename']) && is_string($config['rename']) && preg_match('@^\w+$@', $config['rename']) ) + $rename = $config['rename']; + + /* (1) On récupère si le paramètre est optionnel ou pas */ + $optional = isset($config['optional']) && $config['optional'] === true; + + /* (2) Si de type 'FILE' + fichier existe => on enregistre la ref. */ + if( $config['type'] == 'FILE' && isset($_FILES[$name]) ) + $this->params[$rename] = &$_FILES[$name]; + + /* (3) Si param obligatoire et manquant -> erreur */ + if( !isset($this->raw_params[$name]) && !$optional ) + return $this->error->set(Err::MissingParam, $name); + + + /* (2.3) Gestion des valeurs + ---------------------------------------------------------*/ + /* (1) Si le paramètre est optionnel et manquant */ + if( $optional && !isset($this->raw_params[$name]) ){ + + // On le crée le param optionnel avec la valeur NULL + $this->params[$rename] = null; + + /* (2) Si le paramètre est renseigné (sauf FILE) */ + }elseif( $config['type'] != 'FILE'){ + + // Si la verification est fausse, on retourne faux + if( !Checker::run($config['type'], $this->raw_params[$name]) ) + return $this->error->set(Err::WrongParam, $name, $config['type']); + + // Sinon, on ajoute aux params qu'on enverra à l'appel + else + $this->params[$rename] = $this->raw_params[$name]; + + } + + } + + + /* (3) Gestion du retour, si tout s'est bien passe + ---------------------------------------------------------*/ + return true; + + } + + + + /* (7) Ajout des options a partir de la configuration + * + * @return correct Retourne FAUS en cas d'erreur + * + ---------------------------------------------------------*/ + private function buildOptions(){ + + /* (1) On récupère les options de la méthode en cours + ---------------------------------------------------------*/ + $method = $this->schema['raw'][$this->path['path']][$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']; + + + /* (2) Gestion des différentes options + ---------------------------------------------------------*/ + foreach($options as $option=>$value){ + + /* (1) On ne prend en compte l'option que si elle est dans les options par défaut */ + if( !isset(self::$default_options[$option]) ) + continue; + + /* (2) Le type de la valeur doit être le même que celui de la valeur par défaut */ + if( gettype($value) != gettype(self::$default_options[$option]) ) + continue; + + /* (3) Si tout est bon, on définit la valeur */ + + $this->options[$option] = $value; + } + + return true; + + } + + + + /* (8) Execute le traitement associe et remplie la reponse + * + * @return answer Retourne une reponse de type si tout s'est bien passe + * + ---------------------------------------------------------*/ + public function dispatch(){ + + /* (1) 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['path']); + + if( $instance instanceof Error ){ + $this->error->set(Err::UncallableModule, $this->path['path']); + return new Response($this->error); + } + + + /* (3) On verifie que la methode est amorcable + ---------------------------------------------------------*/ + if( !is_callable([$instance, $this->path['method']]) ){ + $this->error->set(Err::UncallableMethod, $this->path['method']); + return new Response($this->error); + } + + + /* (4) On amorce la methode + ---------------------------------------------------------*/ + /* (1) On lance la fonction */ + $returned = call_user_func( [$instance, $this->path['method']], $this->params ); + + /* (2) On appelle le destructeur (si défini) */ + $instance = null; + + + /* (5) Gestion de la reponse + ---------------------------------------------------------*/ + /* (1) S'il s'agit d'un téléchargement -> on dispatch */ + if( $this->options['download'] === true ) + return $this->download(); + + /* (2) On construit la réponse avec l'erreur */ + $response = new Response($this->error); + + /* (3) On ajoute les données */ + $response->appendAll($returned); + + /* (4) On retourne la réponse */ + return $response; + + } + + + + /* EXECUTE LE TRAITEMENT ASSOCIE ET RENVOIE UN FICHIER AVEC LE HEADER ET LE BODY SPECIFIE + * + */ + public function download(){ + + /* (1) Vérification des erreurs et paramètres + ---------------------------------------------------------*/ + /* (1) Si retourne 'error' et n'est pas SUCCESS -> error */ + if( isset($returned['error']) && $returned['error'] instanceof Error && $returned['error']->get() != Err::Success ){ + $this->error = $returned['error']; + return new Response($this->error); + } + + /* (2) Vérification du contenu, si pas défini */ + if( !isset($returned['body']) ){ + $this->error->set(Err::MissingBody); + return new Response($this->error); + } + + /* (3) Si @headers n'est pas défini on met par défaut */ + if( !isset($returned['headers']) || !is_array($returned['headers']) ){ + $this->error->set(Err::MissingHeaders); + return new Response($this->error); + } + + /* (4) Détermine si téléchargement AJAX/DIRECT */ + $from_ajax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; + + + /* (2) Gestion du téléchargement direct (HTTP) + ---------------------------------------------------------*/ + if( !$from_ajax ){ + + /* (1) On définit les headers */ + foreach($returned['headers'] as $header=>$value) + header($header.': '.$value); + + /* (2) On affiche le contenu */ + echo $returned['body']; + + return true; + + } + + + /* (3) Gestion du téléchargement différé (AJAX) + ---------------------------------------------------------*/ + /* (1) On génère les noms de fichiers utiles */ + $target_fname = '/tmp/download_'.uniqid().'.php'; // cible + $buffer_fname = __ROOT__.'/tmp/content_'.uniqid().'.php'; // buffer + + /* (2) On écrit le BODY dans un fichier buffer */ + $buffer_file = fopen($buffer_fname, 'w'); + fwrite($buffer_file, $returned['body']); + fclose($buffer_file); + + /* (3) On crée le fichier cible */ + $target_fnameroot = __PUBLIC__.$target_fname; + $taret_file = fopen($target_fnameroot, 'w'); + fwrite($taret_file, '$value) + fwrite($taret_file, "header(\"$header: $value\");".PHP_EOL); + + /* (5) Script qui écrira le contenu du buffer */ + chmod($buffer_fname, 0775); + fwrite($taret_file, "readfile('$buffer_fname');".PHP_EOL); + + /* (6) Script qui supprimera les fichiers: buffer+target */ + fwrite($taret_file, "unlink('$buffer_fname');".PHP_EOL); + fwrite($taret_file, "unlink(__FILE__);".PHP_EOL); + fwrite($taret_file, '?>'.PHP_EOL); + + /* (7) On ferme le fichier cible */ + fclose($taret_file); + chmod($target_fnameroot, 0775); + + /* (8) On envoie la réponse contenant le lien du fichier cible */ + $response = new Response($this->error); + $response->append('link', $target_fname); + + return $response; + + } + + + } + +?> diff --git a/src/packages/api/3.0/core/Response.php b/src/packages/api/3.0/core/Response.php new file mode 100755 index 0000000..5cd3e82 --- /dev/null +++ b/src/packages/api/3.0/core/Response.php @@ -0,0 +1,133 @@ + Erreur passee par la requete (si existe) + * + */ + public function __construct($error=null){ + if( !( $error instanceof Error ) ) + $error = new Error(Err::Success); + + $this->data = []; + $this->error = $error; + } + /* AJOUTE UNE DONNEE A LA REPONSE + + + * + * @key Le nom de la valeur a ajouter + * @value La valeur a ajouter + * + */ + public function append($key, $value){ + // Ajoute une entree pour la cle @key et de valeur @value + $this->data[$key] = $value; + + return $this; + } + + + /* AJOUTE TOUTES LES DONNEES A LA REPONSE + * + * @dataset Le tableau associatif correspondant a la reponse + * + */ + public function appendAll($dataset){ + // Si ce n'est pas un tableau, on ne fais rien + if( !is_array($dataset) ) + return $this; + + // Si une valeur contient une erreur + if( array_key_exists('error', $dataset) && $dataset['error'] instanceof Error){ + // On definit cette erreur + $this->error = $dataset['error']; + // On enleve cette entree des donnees + unset($dataset['error']); + } + + // Ajoute une entree pour la cle @key et de valeur @value + $this->data = $dataset; + + return $this; + } + /* RECUPERE UNE DONNEE DE LA REPONSE + + + * + * @key Le nom de la valeur a recuperer + * + * @return value La valeur a cette cle + * @return error Retourne NULL si aucune valeur pour cette cle + * + */ + public function get($key){ + // Si la valeur de cle @key n'existe pas, on retourne NULL + if( !isset($this->data[$key]) ) + return null; + + // Sinon, on retourne la valeur associee + return $this->data[$key]; + } + + + /* RECUPERE TOUTES LES DONNEES DE LA REPONSE + * + * @return data Les donnees de la reponse + * + */ + public function getAll(){ + // Sinon, on retourne la valeur associee + return $this->data; + } + + + /* SERIALISATION A PARTIR DES DONNEES + * + * @return json Retourne les donnees serialisees + * + */ + public function serialize(){ + + // Code Http + $this->error->setHttpCode(); + + // Type de contenu + header('Content-Type: application/json; charset=utf-8'); + + // On rajoute l'erreur au message + $returnData = array_merge([ + 'error' => $this->error->get(), + 'ErrorDescription' => $this->error->explicit() + ], + $this->data + ); + + return json_encode($returnData); + + } + + } + +?> diff --git a/src/packages/database/3.0/core/DatabaseDriver.php b/src/packages/database/3.0/core/DatabaseDriver.php new file mode 100644 index 0000000..83007eb --- /dev/null +++ b/src/packages/database/3.0/core/DatabaseDriver.php @@ -0,0 +1,197 @@ + Database Server's host + * @dbname Database name + * @username Database username + * @password Database password + * + */ + private function __construct($host, $dbname, $username, $password){ + /* (2) Stores configuration */ + $this->host = $host; + $this->dbname = $dbname; + $this->username = $username; + $this->password = $password; + + try{ + + $this->pdo = new \PDO('mysql:host='.$this->host.';dbname='.$this->dbname, $this->username, $this->password, [ + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_TIMEOUT => 5 + ]); + + // On signale que tout s'est bien passe + $this->error = new Error(Err::Success); + + }catch(Exception $e){ + // On signale qu'il y a une erreur + $this->error = new Error(Err::PDOConnection); + } + } + + + + /************************************************ + **** Multiton Management (static) **** + ************************************************/ + + /* ADDS A NEW CONNECTION + * + * @label [optional] Database Label + * + * @return status If added successfully + * + */ + private static function add($label=null){ + $conf = self::conf(); + + /* [1] Default values + =========================================================*/ + /* (1) If label isn't given */ + is_null($label) && ($label = 'default'); + + /* (2) If label and no path */ + if( $label !== 'default' && !isset($conf[$label]) ) + return false; + + + /* [3] Instanciates the driver + =========================================================*/ + try{ + + /* (1) If local -> instanciates with local configuration */ + // if( !checkdnsrr($_SERVER['SERVER_NAME'], 'NS') ) + self::$instance[$label] = new DatabaseDriver($conf[$label]['local']['host'], $conf[$label]['local']['dbname'], $conf[$label]['local']['user'], $conf[$label]['local']['password']); + /* (2) If Remote -> instanciates with Remote configuration */ + // else + // self::$instance[$label] = new DatabaseDriver($conf[$label]['remote']['host'], $conf[$label]['remote']['dbname'], $conf[$label]['remote']['user'], $conf[$label]['remote']['password']); + + return true; + + }catch(\Exception $e){ + + /* (3) If fails */ + return false; + + } + + } + + + /* GET A DATABASE DRIVER INSTANCE + * + * @label [optional] Driver's label + * + * @return driver Multiton + * + */ + public static function get($label=null){ + $conf = self::conf(); + + /* [1] Checks arguments + =========================================================*/ + /* (1) Label default value */ + is_null($label) && ($label = 'default'); + + /* (2) If no label, or unknown label */ + if( is_null($label) || !isset(self::$instance[$label]) ){ + + /* (2.1) Try to add the configuration if exists */ + if( isset($conf[$label]) ){ + self::add($label); + return self::get($label); + } + + + throw new \Exception('Database @label is incorrect.'); + } + + + /* [2] Returns instance + =========================================================*/ + return self::$instance[$label]; + } + + + /** retourne la connection statique + * @param null $label + * @return \PDO + */ + public static function getPDO($label=null){ + $instance = self::get($label); + + return $instance->pdo; + } + + + public function pdo(){ + return $this->pdo; + } + + + public function getConfig(){ + return [ + 'host' => $this->host, + 'dbname' => $this->dbname, + 'username' => $this->username + ]; + } + + + + } +?> diff --git a/src/packages/database/3.0/core/Repo.php b/src/packages/database/3.0/core/Repo.php new file mode 100644 index 0000000..d591b3d --- /dev/null +++ b/src/packages/database/3.0/core/Repo.php @@ -0,0 +1,79 @@ +pdo()); + + + /* (3) Check if the method exists */ + if( !\method_exists($instance, $method) ) + throw new \Exception("Repo '$repo' has no public method '$method'"); + + /* (4) Fetch response (send arguments as well) */ + $response = call_user_func_array([$instance, $method], array_slice(func_get_args(), 2)); + + /* (5) Call post-script */ + $instance = null; + + /* (6) Dispatch response */ + return $response; + + } + + + + + } \ No newline at end of file diff --git a/src/packages/database/3.0/core/Repo_i.php b/src/packages/database/3.0/core/Repo_i.php new file mode 100644 index 0000000..5b9aad5 --- /dev/null +++ b/src/packages/database/3.0/core/Repo_i.php @@ -0,0 +1,18 @@ +pdo = $pdo; + + } + + } \ No newline at end of file diff --git a/src/packages/error/2.0/core/Err.php b/src/packages/error/2.0/core/Err.php index bd7a56c..720190b 100644 --- a/src/packages/error/2.0/core/Err.php +++ b/src/packages/error/2.0/core/Err.php @@ -63,6 +63,8 @@ const WrongParam = 17; /* (12) Erreur dans le traitement */ const ModuleError = 18; + /* (13) URI Invalide */ + const InvalidURI = 19; /* [5] Database @@ -70,40 +72,50 @@ /* (1) Base de données ---------------------------------------------------------*/ /* (1) Erreur lors de la creation d'un objet PDO (connection) */ - const PDOConnection = 19; + const PDOConnection = 20; /* (2) Repositories ---------------------------------------------------------*/ /* (1) Verification de la coherence du chemin (existe dans la conf) */ - const WrongPathRepo = 20; + const WrongPathRepo = 21; /* (2) Module non specifie dans la conf */ - const UnknownRepo = 21; + const UnknownRepo = 22; /* (3) Erreur dans le traitement */ - const RepoError = 22; + const RepoError = 23; /* (3) ORM ---------------------------------------------------------*/ /* (1) Table n'existe pas */ - const UnknownTable = 23; + const UnknownTable = 24; /* (2) Pas permissions de lire le schéma */ - const NotAllowedSchema = 24; + const NotAllowedSchema = 25; /* [6] Erreurs diverses =========================================================*/ + /* (1) Aucune donnée trouvée */ - const NoMatchFound = 25; + const NoMatchFound = 26; /* (2) Mauvais chemin de template */ - const UnknownTemplate = 26; + const UnknownTemplate = 27; /* (3) géolocalisation échouée */ - const UnknownAddress = 27; + const UnknownAddress = 28; /* (4) Erreur inconnue */ - const UnknownError = 28; + const UnknownError = 29; + + /* (5) Entrée existante */ + const AlreadyExists = 30; + + /* (6) Corps manquant */ + const MissingBody = 31; + + /* (7) Header manquant */ + const MissingHeaders = 32; } ?> diff --git a/src/packages/error/2.0/core/Error.php b/src/packages/error/2.0/core/Error.php index 70f9944..8f597ff 100644 --- a/src/packages/error/2.0/core/Error.php +++ b/src/packages/error/2.0/core/Error.php @@ -78,6 +78,7 @@ case Err::MissingParam: return $this->MissingParam(); break; case Err::WrongParam: return $this->WrongParam(); break; case Err::ModuleError: return $this->ModuleError(); break; + case Err::InvalidURI: return $this->InvalidURI(); break; case Err::PDOConnection: return $this->PDOConnection(); break; case Err::WrongPathRepo: return $this->WrongPathRepo(); break; case Err::UnknownRepo: return $this->UnknownRepo(); break; @@ -88,6 +89,9 @@ case Err::UnknownTemplate: return $this->UnknownTemplate(); break; case Err::UnknownAddress: return $this->UnknownAddress(); break; case Err::UnknownError: return $this->UnknownError(); break; + case Err::AlreadyExists: return $this->AlreadyExists(); break; + case Err::MissingBody: return $this->MissingBody(); break; + case Err::MissingHeaders: return $this->MissingHeaders(); break; default: return $this->UnknownDebugError(); break; } @@ -110,7 +114,10 @@ }private function TokenError(){ return 'bad or expired token'; }private function PermissionError(){ - return 'permission error'; + if( count($this->arguments) > 0 ) + return "missing permission: '".$this->arguments[0]."'"; + else + return 'permission error'; }private function DisabledModule(){ return 'disabled module'; }private function MissingPath(){ @@ -155,7 +162,12 @@ else return 'wrong param'; }private function ModuleError(){ - return 'module error'; + if( count($this->arguments) > 0 ) + return 'module error: \''.$this->arguments[0].'\''; + else + return 'module error'; + }private function InvalidURI(){ + return 'invalid URI'; }private function PDOConnection(){ return 'database error'; }private function WrongPathRepo(){ @@ -176,6 +188,12 @@ return 'unknown'; }private function UnknownError(){ return 'unknown error'; + }private function AlreadyExists(){ + return 'item already exists'; + }private function MissingBody(){ + return 'body is missing'; + }private function MissingHeaders(){ + return 'headers are missing'; }private function UnknownDebugError(){ return 'unknown debug error'; } diff --git a/src/packages/orm/0.8.3/core/Rows.php b/src/packages/orm/0.8.3/core/Rows.php new file mode 100755 index 0000000..0bd7592 --- /dev/null +++ b/src/packages/orm/0.8.3/core/Rows.php @@ -0,0 +1,1121 @@ +__'; + const COND_INF = '__<__'; + const COND_SUP = '__>__'; + const COND_INFEQ = '__<=__'; + const COND_SUPEQ = '__>=__'; + const COND_LIKE = '__LIKE__'; + const COND_IN = '__IN__'; + + // {2} Fonctions d'aggrégation // + const SEL_AVG = '__AVG__'; + const SEL_SUM = '__SUM__'; + const SEL_MAX = '__MAX__'; + const SEL_MIN = '__MIN__'; + const SEL_COUNT = '__COUNT__'; + const SEL_CONCAT = '__GROUP_CONCAT__'; + + const SEL_DISTINCT = true; + + // {3} Gestion du Order By // + const ORDER_ASC = '__ASC__'; + const ORDER_DESC = '__DESC__'; + + // {3} Constantes d'insertion // + const INSERT_DEFAULT = '__DEFAULT__'; // Valeur DEFAULT (pour insertion) + const NULL = '__NULL__'; // Valeur DEFAULT (pour insertion) + + /* Attributs */ + private $driver; // Database driver label + private $where; // Tableau associatif contenant les conditions + private $select; // Tableau contenant la liste des champs à afficher + private $orderby; // Tableau contenant la liste des orderby + private $unique; // VRAI si on attend une valeur unique + private $schema; // Tableau contenant les informations associées aux données + private $joined; // Tableau contenant les Rows liés + + + /* CONSTRUCTEUR + * + * @schema Tableau contenant les informations de la requête + * @driver [optional] DatabaseDriver label + * + */ + public function __construct($schema, $driver=null){ + /* (1) Database Driver */ + $this->driver = $driver; + + /* (2) On récupère les informations */ + $this->schema = $schema; + + /* (3) On initialise les conditions */ + $this->where = []; + + /* (4) On initialise les champs à retourner */ + $this->select = []; + + /* (5) On initialise l'ordonnancement' */ + $this->orderby = []; + + /* (6) On initialise le caractère 'unique' du résultat */ + $this->unique = false; + + /* (7) On initialise les jointures */ + $this->joined = []; + } + + + /* FILTRE LES ENTREES D'UNE TABLE AVEC LA CLE PRIMAIRE SPECIFIEE + * + * @primary Clé primaire simple + * OU + * @primary Clé primaire composée + * + * @return Rows Tableau contenant toutes les entrées de la table + * + */ + public function whereId($primary){ + /* [0] Vérification des paramètres + =========================================================*/ + if( $primary == null ) + return $this; + + /* [1] On récupère les clés primaires + =========================================================*/ + $keys = []; + + foreach($this->schema['columns'] as $k=>$v) + if( $v['primary'] ) $keys[] = $k; + + + /* [2] Si clé simple + =========================================================*/ + /* (1) On met au même format qu'une clé composée */ + if( count($keys) == 1 ) + $primary = [ $primary ]; + + + + /* [3] Si clé composée + =========================================================*/ + $defaultWhere = $this->where; + + /* (1) Pour chaque clé, On vérifie les TYPES */ + foreach($keys as $i=>$key){ + + $inCond = is_array($primary[$i]) && count($primary[$i]) >= 2 && is_array($primary[$i][0]) && $primary[$i][1] == self::COND_IN; + + /* (1) Si c'est une condition "IN" + ---------------------------------------------------------*/ + if( $inCond ){ + + /* (1) On vérifie le type de chaque valeur du IN */ + $type = $this->schema['columns'][$key]['type']; + + foreach($primary[$i][0] as $value){ + if( $type == 'int' && !is_numeric($value) ){ $this->where = $defaultWhere; return $this; } + if( $type == 'float' && !is_numeric($value) ){ $this->where = $defaultWhere; return $this; } + if( in_array($type, ['text', 'varchar']) && !is_string($value) ){ $this->where = $defaultWhere; return $this; } + } + + /* (2) Si c'est une condition "simple" + ---------------------------------------------------------*/ + }else{ + + /* (1) Si le type de condition est manquant, on met EQUAL par défaut */ + if( !is_array($primary[$i]) ) + $primary[$i] = [ $primary[$i], self::COND_EQUAL ]; + + /* (2) On vérifie le type de chaque valeur */ + $type = $this->schema['columns'][$key]['type']; + + if( $type == 'int' && !is_numeric($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; } + if( $type == 'float' && !is_numeric($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; } + if( in_array($type, ['text', 'varchar']) && !is_string($primary[$i][0]) ){ $this->where = $defaultWhere; return $this; } + + } + + + /* (6) Si type OK, on enregistre la condition */ + if( !isset($this->where[$key]) ) + $this->where[$key] = []; + + /* (7) On ajoute la condition */ + $this->where[$key][] = $primary[$i]; + + } + + + + + /* [4] On renvoie l'object courant + =========================================================*/ + return $this; + } + + + /* FILTRAGE DYNAMIQUES + * + * @method Nom de la méthode + * @parameter Valeur du paramètre + * @parameter Valeur du paramètre + type de vérification (tableau) + * + * @return this Retourne l'object courant + * + */ + public function __call($m, $a){ + /* [0] On vérifie que la requête est du type 'getBy{Attribute}' + =========================================================*/ + if( !preg_match('/^where(.+)$/', $m, $regex) ) // si requête incorrecte, on ne fais rien + return $this; + + + /* [1] On récupère le nom de la colonne + =========================================================*/ + $column_name = ''; + + /* (1) formatte la requête 'MyAttribute' -> 'my_attribute' */ + for( $l = 0, $ll = strlen($regex[1]) ; $l < $ll ; $l++ ){ + $letter = $regex[1][$l]; + + // Si la lettre est en majuscule mais que c'est pas la première ni un seul mot + if( strtoupper($letter) == $letter && $l > 0 ) + $column_name .= '_'; + + $column_name .= strtolower($letter); + } + + + /* (2) On vérifie que la colonne existe */ + if( !isset($this->schema['columns'][$column_name]) ) + return $this; // si n'existe pas, on ne fait rien + + + /* [2] On vérifie le type du paramètre + =========================================================*/ + // On délègue + $args = array_merge([$column_name], $a); + return call_user_func_array([$this, 'where'], $args); + } + + + public function where($field){ + // get arguments + $args = array_slice(func_get_args(), 1); + + /* [1] Vérification de l'argument @field + =========================================================*/ + /* (1) Type de @field */ + if( !is_string($field) ) + return $this; + + /* (2) On vérifie que la colonne existe */ + if( !isset($this->schema['columns'][$field]) ) + return $this; // si n'existe pas, on ne fait rien + + /* [2] On vérifie le type du paramètre + =========================================================*/ + /* (1) Si au moins 1 param */ + if( count($args) < 1 ) + return $this; + + + /* [3] If `IN` condition + =========================================================*/ + $defaultWhere = $this->where; + $inCond = count($args[0]) > 1 && is_array($args[0][0]) && $args[0][1] == self::COND_IN; + + // erreur + if( is_array($args[0][0]) && !$inCond ) + return $this; + + /* (1) Si c'est une condition "IN" + ---------------------------------------------------------*/ + if( $inCond ){ + + /* (1) On vérifie le type de chaque valeur du IN */ + $type = $this->schema['columns'][$field]['type']; + + foreach($args[0][0] as $value){ + if( $type == 'int' && !is_numeric($value) ) return $this; + if( $type == 'float' && !is_numeric($value) ) return $this; + if( in_array($type, ['text', 'varchar']) && !is_string($value) ) return $this; + } + + /* (2) Si c'est une condition "simple" + ---------------------------------------------------------*/ + }else{ + + /* (1) Si le type de condition est manquant, on met EQUAL par défaut */ + if( !is_array($args[0]) ) + $args[0] = [ $args[0], self::COND_EQUAL ]; + + /* (2) On vérifie le type de chaque valeur */ + $type = $this->schema['columns'][$field]['type']; + + if( !is_null($args[0][0]) ){ + + if( $type == 'int' && !is_numeric($args[0][0]) ) return $this; + if( $type == 'float' && !is_numeric($args[0][0]) ) return $this; + if( in_array($type, ['text', 'varchar']) && !is_string($args[0][0]) ) return $this; + + } + + } + + + /* [3] Si type OK, on enregistre la condition + =========================================================*/ + /* (1) Si aucune condition pour ce champ, on crée un tableau */ + if( !isset($this->where[$field]) ) + $this->where[$field] = []; + + /* (2) On ajoute la condition */ + $this->where[$field][] = $args[0]; + + // On retourne l'object courant + return $this; + } + + /* SELECTIONNE UNIQUEMENT LE CHAMP SELECTIONNE + * + * @field Libellé du champ à afficher + * @func Fonction d'aggrégation (ou NULL) + * @distinct Clause DISTINCT + * @alias Alias du champ + * + * @return this Retourne l'object courant + * + */ + public function select($field=null, $func=null, $distinct=false, $alias=null){ + /* [1] On formatte les champs + =========================================================*/ + /* (1) On vérifie le type de @field */ + if( !is_string($field) ) + return $this; + + /* (2) On vérifie que la colonne @field existe, sinon on quitte */ + if( !isset($this->schema['columns'][$field]) && $field != '*' ) + return $this; + + /* (3) On vérifie @func */ + $funcList = [self::SEL_AVG, self::SEL_SUM, self::SEL_MAX, self::SEL_MIN, self::SEL_COUNT, self::SEL_CONCAT]; + + // Si condition non nulle et pas référencée, on quitte + if( !is_null($func) && !in_array($func, $funcList) ) + return $this; + + // If CONCAT -> force type to TEXT + if( $func === Rows::SEL_CONCAT ) + $this->schema['columns'][$field]['type'] = 'text'; + + /* (4) On met la valeur par défaut à @distinct si type mauvais */ + $distinct = !is_bool($distinct) ? false : $distinct; + + /* (5) Si @alias incorrect, on met @field par défaut */ + if( !is_string($alias) ) + $alias = $field; + + /* [2] On enregistre le champ + =========================================================*/ + /* (1) Si "SELECT *" on ajout tous les champs */ + if( $field === '*' ){ + + foreach($this->schema['columns'] as $f=>$c) + if( !isset($this->select[$f]) ) + $this->select[$f] = [$func, $distinct, $f]; + + /* (2) Si aucun SELECT pour ce champ, on le crée */ + }else{ + + if( !isset($this->select[$field]) ) + $this->select[$field] = [$func, $distinct, $alias]; + } + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + /* SELECTIONNE L'ORDONNANCEMENT DES RESULTATS + * + * @field Libellé du champ à afficher + * @order Gestion de l'ordre ASC/DESC (ou NULL) + * + * @return this Retourne l'object courant + * + */ + public function orderby($field=null, $order=null){ + /* [1] On formatte les champs + =========================================================*/ + /* (1) On vérifie le type de @field */ + if( !is_string($field) ) + return $this; + + /* (2) On vérifie que la colonne @field existe, sinon on quitte */ + if( !isset($this->schema['columns'][$field]) && $field != '*' ) + return $this; + + /* (3) On vérifie @order */ + $orderList = [self::ORDER_ASC, self::ORDER_DESC]; + + // Valeur si NULL + $order = is_null($order) ? $orderList[0] : $order; + + // Si ordre non référencée, on quitte + if( !in_array($order, $orderList) ) + return $this; + + + /* [2] On enregistre le champ + =========================================================*/ + /* (1) On crée le ORDER_BY pour ce champ */ + $this->orderby[$field] = $order; + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + /* JOINT UNE SECONDE TABLE () + * + * @localField Nom d'une colonne locale + * @rows Rows d'une autre table + * + * @return this Retourne l'object courant + * + */ + public function join($localField, $rows){ + /* [0] Vérification / Formattage des paramètres + =========================================================*/ + /* (1) Si le champ n'est pas au bon format */ + if( !is_string($localField) ) + return $this; + + /* (2) Si @rows n'est pas au bon format */ + if( !($rows instanceof Rows) ) + return $this; + + /* (3) Si le champ n'existe pas dans la table */ + if( !isset($this->schema['columns'][$localField]) ) + return $this; + + /* (4) On récupère les données du champ local dans une variable */ + $localFieldData = $this->schema['columns'][$localField]; + + /* [1] On vérifie que la clé étrangère est correcte + =========================================================*/ + /* (1) Si la colonne n'existe pas et qu'elle n'est pas primaire, on ne fait rien */ + if( !isset($localFieldData['references']) && !$localFieldData['primary'] ) + return $this; + + /* (2) On vérifie que la colonne a une référence vers la table de @rows */ + $referencesToRows = isset($localFieldData['references']) && $localFieldData['references'][0] == $rows->schema['table']; + $rowsField = null; + + /* (3) On vérifie que la colonne est la référence d'un champ de @rows */ + $referencesFromRows = false; + + // On vérifie chaque champ de @rows + foreach($rows->schema['columns'] as $field=>$data) + // Si un champ de la table de @rows a pour référence le champ local + if( isset($data['references']) && $data['references'][0] == $this->schema['table'] && $data['references'][1] == $localField ){ + $referencesFromRows = true; + $rowsField = $field; + break; + } + + /* (4) On vérifie que la colonne a la même référence qu'une colonne de @rows */ + $referencesSameTarget = false; + + // On vérifie toutes les colonnes de @rows + foreach($rows->schema['columns'] as $field=>$data) + // Si on trouve un champ avec la même référence + if( isset($data['references']) && isset($localFieldData['references']) && count(array_diff($data['references'], $localFieldData['references'])) == 0 ){ + $referencesSameTarget = true; + $rowsField = $field; // On enregistre le champ qui a la même cible + break; + } + + /* (4) Si aucune référence en commun, on ne fait rien */ + if( !$referencesToRows && !$referencesFromRows && !$referencesSameTarget ) + return $this; + + + /* [2] On enregistre la référence + =========================================================*/ + $this->joined[$localField] = [ + 'object' => $rows, + 'field' => is_null($rowsField) ? $localFieldData['references'][1] : $rowsField // On met le nom du champ de @rows à lier + ]; + + + /* [3] On retourne l'object courant + =========================================================*/ + return $this; + } + + + /* PERMET DE DIRE QUE L'ON VEUT UN RESULTAT UNIQUE + * + * @return this Retourne l'object courant + * + */ + public function unique(){ + /* [1] On enregistre le choix + =========================================================*/ + $this->unique = true; + + + /* [2] On retourne l'object courant + =========================================================*/ + return $this; + } + + + /* MODIFIE DES ENTREES (SANS MODIFICATION DE CLE PRIMAIRE POSSIBLE) + * + * @updates Tableau associatif contenant les nouvelles valeurs + * + * @return updated Retourne si TRUE/FALSE la modification a bien été faite + * + */ + public function edit($updates){ + /* [0] Vérification des paramètres + =========================================================*/ + /* (1) Si c'est pas un tableau, erreur */ + if( !is_array($updates) ) + return false; + + /* (2) On retire les champ inconnus / clés primaires */ + $cleared = []; + + // Pour chaque entrée du tableau + foreach($updates as $field=>$value) + if( isset($this->schema['columns'][$field]) && !$this->schema['columns'][$field]['primary'] ) // Champ existe et n'est pas clé primaire + $cleared[$field] = $value; + + /* (3) On vérifie les types des champs */ + foreach($cleared as $field=>$value){ + + // let null values + if( is_null($value) ) + continue; + + $type = $this->schema['columns'][$field]['type']; + + // {1} Si de type INT/FLOAT et pas numérique, on retire le champ // + if( in_array($type, ['int', 'float']) && !is_numeric($value) ) + unset($cleared[$field]); + + // {2} Si de type TEXT/VARCHAR et pas string, on retire le champ // + if( in_array($type, ['text', 'varchar']) && !is_string($value) ) + unset($cleared[$field]); + + } + + /* (4) Si on a plus de champ, on retourne l'object courant */ + if( count($cleared) == 0 ) + return false; + + + + + /* [1] Initialisation des paramètres + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise les paramètres */ + $bound = []; + + + + /* [2] Rédaction de la clause UPDATE + =========================================================*/ + $requestS['UPDATE'] = SQLBuilder::UPDATE($this->schema['table']); + + + /* [3] Rédaction de la clause SET + =========================================================*/ + /* (1) On met tout les champs à modifier */ + $requestS['SET'] = SQLBuilder::SET($cleared, $bound); + + + /* [4] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $requestS['WHERE'] = []; + $c = 0; + foreach($this->where as $field=>$conditions) + foreach($conditions as $cdt=>$value){ + + if( $value[1] == self::COND_IN ) // Si condition de type IN + $requestS['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + /* (2) On ajoute les jointures */ + // Note: On ajoute les requêtes des tables de jointures dans la clause WHERE // + foreach($this->joined as $field=>$data){ + // {1} On récupère la requête/les params de chaque jointure // + $joinedFetched = $data['object']->fetch(false); + + // {2} On met la clé étrangère pour la clause SELECT // + $joinedFetched['request']['SELECT'] = [ $data['object']->schema['table'].'.'.$data['field'] ]; + + // {3} On construit la nouvelle requête // + $joinedRequest = SQLBuilder::BUILD($joinedFetched['request']); + + // {4} On supprime les retours à la ligne // + $joinedRequest = str_replace("\n", " ", $joinedRequest); + + // {5} On l'ajoute à la clause FROM avec comme alias le nom de la table de @data['object'] // + $requestS['WHERE'][] = $this->schema['table'].".$field in ($joinedRequest)"; + + // {6} On ajoute les variables à la requête courante // + $bound = array_merge($bound, $joinedFetched['bound']); + } + + + /* [5] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [6] On prépare et compose la requête + =========================================================*/ + /* (1) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + + /* (2) On prépare la requête */ + $request = DatabaseDriver::getPDO($this->driver)->prepare($requestString); + + + + /* [7] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $updated = $request->execute($bound); + + /* (2) On retourne l'état de la requête */ + return $updated; + } + + /* AJOUTE UNE ENTREE DANS LA TABLE + * + * @entry Tableau associatif de la forme (colonne => valeur) + * OU + * @entries Tableau de la forme ([entry1, entry2]) + * + * @return status Retourne si TRUE ou FALSE les entrées ont bien été supprimées + * + */ + public function insert($entry){ + /* [0] On vérifie les paramètres + =========================================================*/ + /* (1) Si c'est pas un tableau avec au moins une entrée, erreur */ + if( !is_array($entry) || count($entry) == 0 ) + return false; + + // S'il n'y a qu'une entrée, on met au même format que s'il y en avait plusieurs + $firstIndex = array_keys($entry)[0]; + if( !is_array($entry[$firstIndex]) ) + $entry = [ $entry ]; + + /* (2) On retire les champ inconnus */ + $cleared = []; + + // Pour chaque entrée du tableau + foreach($entry as $i=>$set){ + $cleared[$i] = []; + + foreach($set as $field=>$value){ + + if( isset($this->schema['columns'][$field]) ) // Champ existe + $cleared[$i][$field] = $value; + } + + } + + /* (3) On vérifie les types des champs */ + foreach($cleared as $i=>$set){ + + foreach($set as $field=>$value){ + + if( is_null($value) ) continue; + + $type = $this->schema['columns'][$field]['type']; + + // {1} Si de type INT/FLOAT et pas numérique, on retire le champ // + if( in_array($type, ['int', 'float']) && !is_numeric($value) && $value != self::INSERT_DEFAULT ) + unset($cleared[$i][$field]); + + // {2} Si de type TEXT/VARCHAR et pas string, on retire le champ // + if( in_array($type, ['text', 'varchar']) && !is_string($value) && $value != self::INSERT_DEFAULT ) + unset($cleared[$i][$field]); + } + + /* (4) Si il manque des données, erreur */ + if( count($cleared[$i]) != count($this->schema['columns']) ) + return false; + + } + + + /* [1] On crée la requête + =========================================================*/ + /* (1) Clause INSERT INTO table */ + $requestS = 'INSERT INTO '.$this->schema['table']."("; + + /* (2) Clause : table(col1, col2, ...) */ + $c = 0; + foreach($this->schema['columns'] as $field=>$value){ + if( $c > 0 ) $requestS .= ', '; + $requestS .= $field; + + $c++; + } + + // Fin de clause + $requestS .= ")\n"; + + + /* (3) Clause : VALUES(val1, val2, ...) */ + $v = 0; + foreach($cleared as $i=>$set){ + if( $v == 0 ) $requestS .= 'VALUES('; + else $requestS .= ",\n\t("; + + $c = 0; + foreach($this->schema['columns'] as $field=>$column){ + if( $c > 0 ) $requestS .= ', '; + + // Si l'entrée est donnée + if( isset($set[$field]) ) + if( $set[$field] == self::INSERT_DEFAULT ) $requestS .= 'DEFAULT'; // On insère directement les valeurs 'DEFAULT' + else if( is_null($set[$field]) ) $requestS .= 'NULL'; + else $requestS .= ':insert_'.$field.'_'.$i; + else + $requestS .= 'DEFAULT'; + + $c++; + } + + // Fin de clause + $requestS .= ")"; + $v++; + + } + + + + /* [2] On bind les paramètres et exécute la requête + =========================================================*/ + /* (0) On initialise la requête et les paramètres */ + $request = DatabaseDriver::getPDO($this->driver)->prepare($requestS.';'); + $bound = []; + + /* (1) On bind les paramètres */ + foreach($cleared as $i=>$set) + foreach($this->schema['columns'] as $field=>$column) + if( isset($set[$field]) && $set[$field] != self::INSERT_DEFAULT ) + $bound[':insert_'.$field.'_'.$i] = $set[$field]; + + + /* [3] On exécute la requête et envoie le status + =========================================================*/ + $inserted = $request->execute($bound); + + // On retourne le status + return $inserted; + } + + + /* SUPPRIME LES ENTREES + * + * @return status Retourne si TRUE ou FALSE les entrées ont bien été supprimées + * + */ + public function delete(){ + /* [0] Initialisation des paramètres + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise les paramètres */ + $bound = []; + + + /* [1] Clause DELETE FROM + =========================================================*/ + $requestS['DELETE'] = SQLBuilder::DELETE($this->schema['table']); + + + /* [2] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $requestS['WHERE'] = []; + $c = 0; + foreach($this->where as $field=>$conditions) + foreach($conditions as $cdt=>$value){ + + if( $value[1] == self::COND_IN ) // Si condition de type IN + $requestS['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + + /* (2) On ajoute les jointures */ + // Note: On ajoute les requêtes des tables de jointures dans la clause WHERE // + foreach($this->joined as $field=>$data){ + // {1} On récupère la requête/les params de chaque jointure // + $joinedFetched = $data['object']->fetch(false); + + // {2} On met la clé étrangère pour la clause SELECT // + $joinedFetched['request']['SELECT'] = [ $data['object']->schema['table'].'.'.$data['field'] ]; + + // {3} On construit la nouvelle requête // + $joinedRequest = SQLBuilder::BUILD($joinedFetched['request']); + + // {4} On supprime les retours à la ligne // + $joinedRequest = str_replace("\n", " ", $joinedRequest); + + // {5} On l'ajoute à la clause FROM avec comme alias le nom de la table de @data['object'] // + $requestS['WHERE'][] = $this->schema['table'].".$field in ($joinedRequest)"; + + // {6} On ajoute les variables à la requête courante // + $bound = array_merge($bound, $joinedFetched['bound']); + } + + + /* [3] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [4] On prépare et compose la requête + =========================================================*/ + /* (1) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + + /* (2) On prépare la requête */ + $request = DatabaseDriver::getPDO($this->driver)->prepare($requestString); + + /* [5] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $deleted = $request->execute($bound); + + /* (2) On retourne l'état de la requête */ + return $deleted; + } + + + /* RETOURNE LES DONNEES / NULL si une erreur survient + * + * @execute VRAI si on veut exécuter la requête, sinon renvoie [requete, boundParams] + * + * @return data Tableau contenant les champs sélectionnés + * @return data Valeur du champ sélectionné (si 1 seul champ) + * @return ERROR Retourne FALSE si rien n'est trouvé + * + */ + public function fetch($execute=true){ + /* [0] On initialise + =========================================================*/ + /* (1) On initialise la requête */ + $requestS = []; + + /* (2) On initialise le conteneur des variables "bindés" */ + $bound = []; + + /* (3) On récupère la requête générée par chaque @rows de jointure */ + $joinedFetched = []; + foreach($this->joined as $field=>$data) + $joinedFetched[$field] = $data['object']->fetch(false); + + + /* [1] On rédige la clause SELECT + =========================================================*/ + /* (1) On formatte les données */ + $selectTables = []; + + /* (2) On ajoute les champs locaux */ + $selectTables[$this->schema['table']] = $this->select; + + + /* (4) On ajoute les champs des jointures (récursif)*/ + foreach($joinedFetched as $field=>$data){ + foreach($data['request']['SELECT'] as $table=>$fields) + foreach($fields as $field=>$sel){ + // Si aucune entrée pour cette table, on l'ajoute + if( !isset($selectTables[$table]) ) + $selectTables[$table] = []; + + $selectTables[$table][$field] = $sel; + } + } + + /* (3) On génère la clause SELECT */ + $requestS['SELECT'] = SQLBuilder::SELECT($selectTables); + + + /* [2] On rédige la clause FROM + ========================================================*/ + /* (0) On initialise la clause */ + $requestS['FROM'] = []; + + /* (1) Table locale */ + $requestS['FROM'][] = $this->schema['table']; + + /* (2) On ajoute les tables de jointures */ + // Note: On ajoute les tables de jointures dans la clause FROM avec comme alias le nom de la table + foreach($joinedFetched as $field=>$data) + // On ajoute la clause FROM de jointure à la clause FROM locale // + $requestS['FROM'] = array_merge($data['request']['FROM'], $requestS['FROM']); + + /* [5] On rédige la clause WHERE/AND + =========================================================*/ + /* (1) On met les conditions locales */ + $c = 0; + $requestS['WHERE'] = []; + foreach($this->where as $field=>$conditions) + foreach($conditions as $cdt=>$value){ + + if( $value[1] === self::COND_IN ) // Si condition IN + $requestS['WHERE'][$c] = SQLBuilder::IN([$this->schema['table'], $field], $value[0], $c, $bound); + else // Sinon + $requestS['WHERE'][$c] = SQLBuilder::WHERE([$this->schema['table'], $field], $value, $c, $bound); + + $c++; + } + + /* (2) On ajoute les jointures */ + foreach($this->joined as $localField=>$data){ + $requestS['WHERE'][$c] = $this->schema['table'].".$localField = ".$data['object']->schema['table'].".".$data['field']; + $c++; + } + + /* (3) On ajoute les conditions des jointures */ + foreach($joinedFetched as $field=>$data){ + /* On ajoute la clause WHERE de jointure à la clause WHERE locale */ + $requestS['WHERE'] = array_merge($data['request']['WHERE'], $requestS['WHERE']); + + /* On ajoute les variables à la requête courante */ + $bound = array_merge($bound, $data['bound']); + } + + + /* [6] Clause GROUP BY + =========================================================*/ + /* (0) On initialise la liste des @rows non aggrégés */ + $groupBy = []; + + /* (1) On cherche dans les champs locaux local */ + foreach($selectTables as $table=>$fields) + foreach($fields as $field=>$sel) + // Si aucune fonction d'aggrégation + if( is_null($sel[0]) ){ + if( !isset($groupBy[$table]) ) + $groupBy[$table] = []; + + // Si le champ est *, on trouve les clés primaires + if( $field == '*' ){ + $columns = Table::get($table)->schema['columns']; + foreach($columns as $col=>$data) + if( $data['primary'] ) + $groupBy[$table][] = $col; + }else + $groupBy[$table][] = $field; + + + $groupBy[$table] = array_unique($groupBy[$table]); + } + + + /* (2) On rédige la clause GROUP BY */ + if( count($groupBy) > 0) + $requestS['GROUPBY'] = SQLBuilder::GROUPBY($groupBy); + + /* [6] Clause ORDER BY + =========================================================*/ + /* (1) On formatte les données */ + $orderTables = []; + + /* (2) On ajoute les champs locaux */ + if( count($this->orderby) > 0 ) + $orderTables[$this->schema['table']] = $this->orderby; + + /* (4) On ajoute les champs des jointures (récursif)*/ + foreach($joinedFetched as $field=>$data){ + foreach($data['request']['ORDERBY'] as $table=>$fields) // pour chaque ensemble de champ de chaque table + foreach($fields as $field=>$orderBy) // Pour chaque orderby de chaque champ + + if( count($orderBy) > 0 ) + $orderTables[$table][$field] = $orderBy; + + } + + /* (3) On génère la clause SELECT */ + $requestS['ORDERBY'] = SQLBuilder::ORDERBY($orderTables); + + /* [6] Clause LIMIT + =========================================================*/ + $requestS['LIMIT'] = ($this->unique) ? SQLBuilder::LIMIT(1) : SQLBuilder::LIMIT([]); + + + /* [7] On compose/prépare la requête + =========================================================*/ + /* (1) Si on veut pas exécuter on renvoie la requête + boundParams */ + if( !$execute ) + return [ 'request' => $requestS, 'bound' => $bound]; + + /* (2) On compose la requête */ + $requestString = SQLBuilder::BUILD($requestS).';'; + // var_dump($requestString); + + /* (3) On prépare la requête */ + $request = DatabaseDriver::getPDO($this->driver)->prepare($requestString); + + + /* [8] On exécute la requête et retourne le résultat + =========================================================*/ + /* (1) On exécute la requête */ + $request->execute($bound); + + /* (2) Si unique */ + if( $this->unique ) + return $this->format( $request->fetch() ); + + /* (3) Si tout */ + return $this->format( $request->fetchAll() ); + } + + + /* ON FORMATTE LES DONNEES DE SORTIE + * + * @data Données / Tableau de données + * + * @return formatted Données formattées / Tableau de données formatté + * + */ + private function format($data){ + /* [0] On initialise le processus + =========================================================*/ + /* (0) Initialisation du retour */ + $formatted = $data; + + /* (1) On vérifie qu'il s'agit d'un tableau (non vide) */ + if( !is_array($formatted) || count($formatted) < 1 ) + return $formatted; + + /* (2) On regarde si c'est des données simples */ + $twoDimensions = is_array($formatted[0]); + + /* (3) On regarde s'il s'agit d'un Tableau de données en bonne et due forme */ + if( $twoDimensions ){ + $sameKeys = true; // VRAI si chaque entrée a les mêmes clés + $last_keys = null; // Clés de l'entrée précédente + + foreach($formatted as $i=>$entry){ + if( !is_null($last_keys) && count(array_diff(array_keys($entry), $last_keys)) > 0 ){ // Si différent du précédent, ducoup on est pas bon + $sameKeys = false; + break; + } + + $last_keys = array_keys($entry); + } + + // Si pas les mêmes clés, on a une erreur + if( !$sameKeys ) + return $formatted; + } + + + + /* [1] On retire les doublons à indices numériques + =========================================================*/ + /* (1) Si 1 dimensions, on met en 2 pour traiter tout de la même manière */ + if( !$twoDimensions ) + $formatted = [$formatted]; + + + /* (2) On récupère les noms des champs à partir des select (alias) */ + // {1} On récupère les colonnes locales // + $existingColumns = []; + + foreach($this->select as $field=>$data) + $existingColumns[$data[2]] = $this->schema['columns'][$field]; + + + // {2} On ajoute les colonnes des jointures // + foreach($this->joined as $j) + foreach($j['object']->select as $field=>$data) + $existingColumns[$data[2]] = $j['object']->schema['columns'][$field]; + + + // {3} On vérifie chaque clé, si c'est une colonne qui existe // + foreach($formatted as $i=>$entry) + + // Pour chaque champ + foreach($entry as $index=>$value){ + + // Si la colonne existe on applique le type + if( isset($existingColumns[$index]) ){ + + if( $existingColumns[$index]['type'] == 'int' ) + $formatted[$i][$index] = intval( $value ); + elseif( $existingColumns[$index]['type'] == 'float' ) + $formatted[$i][$index] = floatval( $value ); + // String utf8 management + elseif( \mb_detect_encoding($value) === 'UTF-8' ) + $formatted[$i][$index] = $value; + else + $formatted[$i][$index] = utf8_encode($value); + + // Si pas non plus une aggrégation et si indice numérique, on le retire + }else if( !preg_match('/^agg_.+/', $index) && is_numeric($index) ) + unset($formatted[$i][$index]); + + } + + /* (3) On remet 1 dimension si 1 dimension à la base */ + if( !$twoDimensions ) + $formatted = $formatted[0]; + + /* [2] On retourne le résultat + =========================================================*/ + return $formatted; + + } + + } + + + +?> diff --git a/src/packages/orm/0.8.3/core/SQLBuilder.php b/src/packages/orm/0.8.3/core/SQLBuilder.php new file mode 100755 index 0000000..24a907e --- /dev/null +++ b/src/packages/orm/0.8.3/core/SQLBuilder.php @@ -0,0 +1,385 @@ + Liste de champs : [table => field => [func, alias] ] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function SELECT($sqlFields){ + return $sqlFields; + } + + + /* CONSTRUIT LA REQUETE FORMATTEE "ORDER BY" AVEC UNE LISTE DE CHAMPS + * + * @tables Liste de champs : [table => fields] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function ORDERBY($tables){ + return $tables; + } + + /* CONSTRUIT LA REQUETE FORMATTEE "GROUP BY" AVEC UNE LISTE DE CHAMPS + * + * @tables Liste de champs : [table => fields] + * + * @return sql Renvoie un tableau formatté + * + */ + public static function GROUPBY($tables){ + return $tables; + } + + + /* CONSTRUIT LA REQUETE FORMATTEE "FROM" AVEC UNE LISTE DE TABLES + * + * @tables Liste de tables OU SQL PUR + * + * @return sql Renvoie un tableau formatté + * + */ + public static function FROM($tables){ + return $tables; + } + + + /* CONSTRUIT LA REQUETE FORMATTEE "UPDATE" AVEC LA TABLE EN QUESTION + * + * @table Table en question + * + * @return sql Renvoie un tableau formatté + * + */ + public static function UPDATE($table){ + return $table; + } + + + /* CONSTRUIT LA REQUETE FORMATTEE "DELETE" AVEC LA TABLE EN QUESTION + * + * @table Table en question + * + * @return sql Renvoie un tableau formatté + * + */ + public static function DELETE($table){ + return $table; + } + + + /* CONSTRUIT LA REQUETE TEXTUELLE "IN" AVEC UNE LISTE DE TABLES + * + * @field Tableau contenant [table, field] + * @array Valeurs de la clause IN + * @offset Permet de rendre la condition unique (nommage des variables) + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie le textuel formatté + * + */ + public static function IN($field, $array, $offset=0, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + /* [1] On construit la requête + =========================================================*/ + /* (1) Champ */ + $sql .= $field[0].'.'.$field[1].' IN ('; + + /* (2) Valeurs */ + $c = 0; + foreach($array as $i=>$value){ + if( $c > 0 ) $sql .= ', '; + + $sql .= ':'.$field[0].'_x_'.$field[1].'_'.$offset.'_'.$i; + + $bound[':'.$field[0].'_x_'.$field[1].'_'.$offset.'_'.$i] = $value; + + $c++; + } + + return $sql.")"; + } /* CONSTRUIT LA REQUETE TEXTUELLE "WHERE" AVEC UNE LISTE DE TABLES + * + * @field Tableau contenant [table, field] + * @valeur Valeurs de la clause WHERE [valeur, opérateur] + * @offset Permet de rendre la condition unique (nommage des variables) + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie le textuel formatté + * + */ + public static function WHERE($field, $value, $offset=0, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + + /* [1] On construit la requête + =========================================================*/ + /* (1) Chamo */ + $sql .= $field[0].'.'.$field[1].' '; + + /* (2) Opérateur */ + $sql .= substr($value[1], 2, -2).' '; + + /* (3) Variable */ + // {1} Si NULL // + if( is_null($value[0]) ){ + $sql .= 'NULL'; + return $sql; + } + + // {2} Si not NULL // + $sql .= ':'.$field[0].'_x_'.$field[1].'_'.$offset; + + $bound[':'.$field[0].'_x_'.$field[1].'_'.$offset] = $value[0]; + + + return $sql; + } /* CONSTRUIT LA REQUETE FORMATTEE "SET" AVEC UNE LISTE DE TABLES + * + * @values Tableau de la forme [ field=>value, field2=>value2 ] + * @bound Tableau associatif contenant les variables "bindés" -> ajout des champs + * + * @return sql Renvoie un tableau formatté + * + */ + public static function SET($values, &$bound){ + /* [0] Initialisation + =========================================================*/ + $sql = []; + + + /* [1] On construit la requête + =========================================================*/ + $c = 0; + foreach($values as $field=>$value){ + /* (1) Champ */ + $sql[$c] = $field.' = '; + + /* (2) Variable */ + // {1} Si NULL // + if( is_null($value) ){ + $sql[$c] .= 'NULL'; + $c++; + continue; + } + + // {2} Si not NULL // + $sql[$c] .= ':update_'.$field; + + $bound[':update_'.$field] = $value; + + $c++; + } + + return $sql; + } /* CONSTRUIT LA REQUETE FORMATTEE "LIMIT" AVEC UN NOMBRE D'ENTREES + * + * @count Nombre limite + * + * @return sql Renvoie un sql formatté + * + */ + public static function LIMIT($count=null){ + /* [0] Initialisation + =========================================================*/ + $sql = ''; + + + /* [1] On construit la requête + =========================================================*/ + if( intval($count) == $count ) + $sql = intval($count); + + return $sql; + } + + /* CONSTRUIT LA REQUETE A PARTIR D'UNE REQUETTE FORMATTEE + * + * @request Requête formattée + * + * @return sql Requête formattée en SQL + * + */ + public static function BUILD($request){ + /* [0] On initialise le retour + =========================================================*/ + $sql = ''; + + /* [1] Gestion dans l'ordre + =========================================================*/ + foreach($request as $clause=>$statements){ + + switch($clause){ + + /* (1) Clause SELECT + ---------------------------------------------------------*/ + case 'SELECT': + $sql .= "SELECT "; + $c = 0; + foreach($statements as $table=>$fields){ + foreach($fields as $field=>$select){ + + /* (1) On construit le nom du champ */ + $fieldStr = "$table.$field"; + + /* (2) On ajout le DISTINCT s'il y a lieu */ + if( isset($select[1]) && $select[1] ) + $fieldStr = "DISTINCT $fieldStr"; + + /* (3) On ajoute la fonction d'aggrégation s'il y a lieu */ + if( isset($select[0]) && !is_null($select[0]) ) + $fieldStr = substr($select[0], 2, -2)."($fieldStr)"; + + + /* (4) On ajoute l'alias */ + // si défini + if( isset($select[2]) ) + $fieldStr = "$fieldStr as ".$select[2]; + // si func et non défini + elseif( isset($select[0]) && !is_null($select[0]) ) + $fieldStr = "$fieldStr as agg_$field"; + else + $fieldStr = "$fieldStr"; + + $sql .= ($c==0) ? "$fieldStr" : ", $fieldStr"; + + $c++; + } + } + + $sql .= "\n"; + break; + + /* (2) Clause FROM + ---------------------------------------------------------*/ + case 'FROM': + $sql .= 'FROM '; + + $c = 0; + foreach($statements as $field){ + $sql .= ($c==0) ? "$field" : ", $field"; + $c++; + } + + $sql .= "\n"; + break; + + + /* (3) Clause WHERE + ---------------------------------------------------------*/ + case 'WHERE': + $c = 0; + foreach($statements as $field){ + $sql .= ($c==0) ? "WHERE $field\n" : "AND $field\n"; + $c++; + } + + $sql .= ($c==0) ? '' : "\n"; + break; + + + + /* (4) Clause LIMIT + ---------------------------------------------------------*/ + case 'LIMIT': + if( is_numeric($statements) ) + $sql .= 'LIMIT '.intval($statements); + break; + + + /* (5) Clause DELETE + ---------------------------------------------------------*/ + case 'DELETE': + $sql .= "DELETE FROM $statements\n"; + break; + + + /* (6) Clause UPDATE + ---------------------------------------------------------*/ + case 'UPDATE': + $sql .= "UPDATE $statements\n"; + break; + + + /* (7) Clause SET + ---------------------------------------------------------*/ + case 'SET': + $c = 0; + foreach($statements as $field){ + $sql .= ($c>0) ? "\n, $field" : "SET $field"; + $c++; + } + $sql .= "\n"; + break; + + /* (8) Clause GROUP BY + ---------------------------------------------------------*/ + case 'GROUPBY': + $sql .= 'GROUP BY '; + + $c = 0; + foreach($statements as $table=>$fields) + foreach($fields as $field){ + $sql .= ($c==0) ? "$table.$field" : ", $table.$field"; + $c++; + } + + $sql .= "\n"; + break; + + /* (9) Clause ORDER BY + ---------------------------------------------------------*/ + case 'ORDERBY': + + // si aucun ORDER BY, on quitte + if( count($statements) == 0 ) + continue; + + $sql .= 'ORDER BY '; + + $c = 0; + foreach($statements as $table=>$fields) + foreach($fields as $field=>$order){ + + if( $c > 0 ) $sql .= ', '; + + $sql .= "$table.$field ". substr($order, 2, -2); + + $c++; + } + + $sql .= "\n"; + break; + } + + + } + + /* [2] On retourne le résultat + =========================================================*/ + return $sql; + } + + + + } + + +?> diff --git a/src/packages/orm/0.8.3/core/Table.php b/src/packages/orm/0.8.3/core/Table.php new file mode 100755 index 0000000..6154861 --- /dev/null +++ b/src/packages/orm/0.8.3/core/Table.php @@ -0,0 +1,193 @@ + Nom de la table à selectionner + * @driver [optional] DatabaseDriver label + * + * @return this Retourne une instance de l'ORM + * + */ + public static function get($table_name, $driver=null){ + /* [0] Initialisation des attributs + =========================================================*/ + $schema = [ + 'database' => DatabaseDriver::get($driver)->getConfig()['dbname'], + 'table' => null, + 'columns' => null + ]; + + + /* [1] On vérifie que la table existe + =========================================================*/ + /* (1) Requête */ + $checkTable = DatabaseDriver::getPDO($driver)->query("SHOW tables FROM ".$schema['database']); + $checkTableResult = DatabaseDriver::delNumeric( $checkTable->fetchAll() ); + + /* (2) On met en forme les données */ + $tables = []; + foreach($checkTableResult as $table) + $tables[] = $table['Tables_in_'.$schema['database']]; + + /* (3) Si n'existe pas, on renvoie une erreur */ + if( !in_array($table_name, $tables) ) + return null; + + /* (4) On enregistre les données */ + $schema['table'] = $table_name; + + + + /* [2] Si la table existe, on récupère les colonnes + =========================================================*/ + /* (1) On récupère les colonnes */ + $getColumns = DatabaseDriver::getPDO($driver)->query("SHOW columns FROM ".$schema['database'].'.'.$table_name); + $columnsResult = DatabaseDriver::delNumeric( $getColumns->fetchAll() ); + + /* (2) On met en forme les données */ + $columns = []; + foreach($columnsResult as $col){ + // On formatte le type // + $type = $col['Type']; + if( preg_match('/^(int|float|varchar|text)/i', $type, $m) ) + $type = strtolower($m[1]); + + // On ajoute la colonne // + $columns[$col['Field']] = [ + 'type' => $type, + 'primary' => $col['Key'] == 'PRI' + ]; + } + + + /* (3) Si on trouve rien, on envoie une erreur */ + if( !is_array($columns) || count($columns) == 0 ) + return null; + + /* (4) On enregistre les colonnes */ + $schema['columns'] = $columns; + + + + /* [3] On récupère les clés étrangères + =========================================================*/ + /* (1) On récupère le texte du 'CREATE TABLE' */ + $getCreateTable = DatabaseDriver::getPDO($driver)->query("show create table ".$table_name); + $create_table = $getCreateTable->fetch()['Create Table']; + + /* (2) On découpte en lignes */ + $create_table_lines = explode("\n", $create_table); + + /* (3) Pour chaque ligne, si c'est une contrainte, on l'enregistre dans la colonne associée */ + foreach($create_table_lines as $i=>$line) + if( preg_match('/CONSTRAINT `.+` FOREIGN KEY \(`(.+)`\) REFERENCES `(.+)` \(`(.+)`\)+/i', $line, $m) ) + $schema['columns'][$m[1]]['references'] = [$m[2], $m[3]]; + + + /* [3] On renvoie une instance de 'Rows' + =========================================================*/ + return new Rows($schema, $driver); + + } + + + + }; + + + + /*** USE CASE :: ACCESS TABLE `user` ***/ + // ORM::Table('user'); + + + /**** USE CASE :: WHERE ****/ + // WHERE `username` = 'someUsername' + // ORM::Table('user')->whereUsername('someUsername'); + // EQUIVALENT TO + // ORM::Table('user')->whereUsername('someUsername', Rows::COND_EQUAL); + + // WHERE `id_user` < 100 + // ORM::Table('user')->whereIdUser(100, Rows::COND_INF); + + // WHERE `id_user` <= 100 + // ORM::Table('user')->whereIdUser(100, Rows::COND_INFEQ); + + // WHERE `id_user` > 10 + // ORM::Table('user')->whereIdUser(10, Rows::COND_SUP); + + // WHERE `id_user` >= 10 + // ORM::Table('user')->whereIdUser(10, Rows::COND_SUPEQ); + + // WHERE `id_user` in (1, 2, 3, 8) + // ORM::Table('user')->whereIdUser([1, 2, 3, 8], Rows::COND_IN); + + // WHERE `id_user` LIKE 'John %' + // ORM::Table('user')->whereIdUser('John %', Rows::COND_LIKE); + + + /*** USE CASE :: ORDER BY ****/ + // ORDER BY `a` ASC, `b` DESC + // Table::get('someTable') + // ->orderby('a', Rows::ORDER_ASC) + // ->orderby('b', Rows::ORDER_DESC); + // + // Note: `Rows::ORDER_ASC` is set by default if the given FLAG is invalid + + + /**** USE CASE :: SELECT ****/ + // SELECT id_user, username + // Table::get('user') + // ->select('id_user') + // ->select('username'); + + + /**** USE CASE :: AGGREGATION FUNCTIONS ****/ + // SELECT COUNT(`count`) + // Table::get('user')->select('count', Rows::SEL_COUNT) + + // SELECT SUM(distinct `count`) + // Table::get('user')->select('count', Rows::SEL_SUM, Rows::SEL_DISTINCT); + + // SELECT AVG(`count`) + // Table::get('user')->select('count', Rows::SEL_AVG); + + // SELECT MAX(`id_user`) + // Table::get('user')->select('id_user', Rows::SEL_MAX); + + // SELECT MIN(`id_user`) + // Table::get('user')->select('id_user', Rows::SEL_MIN); + + // SELECT GROUP_CONCAT(`count`) + // Table::get('user')->select('count', Rows::SEL_CONCAT); + + + + /**** USE CASE :: FETCH ****/ + // SELECT ... FROM ... WHERE ... ORDERBY ... LIMIT ... + // Table::get('user') + // ->select('id_user') + // ->fetch(); + + // SELECT UNIQUE ... FROM ... WHERE ... ORDERBY ... LIMIT ... + // Table::get('user') + // ->select('id_user') + // ->unique->fetch(); + + + /**** USE CASE :: TABLE JOIN ****/ + // WHERE `user`.`id_user` = `user_merge`.`id_user` + // Table::get('user_merge')->join( + // Table::get('user')->whereIdUser(1, Rows::COND_SUP) + // ); diff --git a/src/packages/token/0.9/core/TreeToken.php b/src/packages/token/0.9/core/TreeToken.php new file mode 100644 index 0000000..ebe5216 --- /dev/null +++ b/src/packages/token/0.9/core/TreeToken.php @@ -0,0 +1,281 @@ + Max. number of children tokens + * + ---------------------------------------------------------*/ + public function __construct($max_step=100){ + /* (1) Check argument */ + if( abs(intval($max_step)) !== $max_step || $max_step < 0 ) + throw new \Exception('Invalid argument type.'); + + /* (2) Manage session */ + if( session_status() != PHP_SESSION_ACTIVE ) + \session_start(); + + /* (3) Set attributes */ + $this->max_step = $max_step+1; + } + + + /* (2) Gets the existing parent + * + * @return status TRUE: right parent + token + * FALSE: Invalid token + * NULL: No parent found + * + ---------------------------------------------------------*/ + private function check_parent(){ + + /* (1) Check system state + ---------------------------------------------------------*/ + $has_token = isset($_COOKIE['_PUBLIC_']) && preg_match( '/^[a-z0-9]{128}$/i', $_COOKIE['_PUBLIC_'] ); + $has_parent = isset($_SESSION['_PRIVATE_']) && preg_match( '/^([a-z0-9]{128})\.(\d+)$/i', $_SESSION['_PRIVATE_'], $p_match ); + + /* (1) If no parent -> NULL */ + if( !$has_parent ) + return null; + + /* (2) But if no token -> FALSE */ + if( !$has_token ) + return false; + + if( self::$DEBUG ){ + echo "* PARENT RECEIVED *\n"; + echo 'sess_id: '.session_id()."\n"; + echo 'pub: '.$_COOKIE['_PUBLIC_']."\n"; + echo 'priv: '.$_SESSION['_PRIVATE_']."\n"; + } + + + /* (2) Check parent token + ---------------------------------------------------------*/ + /* (1) Check public token */ + if( self::tgen($p_match[1], $this->max_step) !== $_COOKIE['_PUBLIC_'] ){ + + if( self::$DEBUG ) + echo "/!\ invalid parent pub (token)\n"; + + return false; + } + + /* (2) Inherit parent properties */ + $this->secret = $p_match[1]; + $this->step = $p_match[2]; + + /* (3) If valid token */ + if( self::$DEBUG ) + echo "[ Valid parent pub (token) ]\n"; + + return true; + + } + + + + + /* (3) Create or regenerate a parent + * + * @inName inDesc + * + * @return outName outDesc + * + ---------------------------------------------------------*/ + public function init_parent(){ + /* (1) Check parent + ---------------------------------------------------------*/ + /* (1) Process check */ + $valid_parent = $this->check_parent(); + + /* (2) If invalid parent -> destroy session */ + if( $valid_parent === false ){ + + if( self::$DEBUG ) + echo "-> new session <-\n"; + + \session_regenerate_id(true); // true: delete old session + \session_unset(); + \session_destroy(); + \session_start(); + } + + + /* (2) Init new parent + ---------------------------------------------------------*/ + /* (1) Choose new secret */ + $this->secret = self::tgen(uniqid()); + + /* (2) Set step = max */ + $this->step = $this->max_step; + + /* (3) Generate PRIVATE */ + $_SESSION['_PRIVATE_'] = $this->secret.'.'.$this->max_step; + + /* (4) Generate PUBLIC */ + $_COOKIE['_PUBLIC_'] = self::tgen($this->secret, $this->max_step); + setcookie('_PUBLIC_', $_COOKIE['_PUBLIC_'], time()+30*60, '/'); + + if( self::$DEBUG ){ + echo "\n* PARENT UPDATED *\n"; + echo 'sess_id: '.session_id()."\n"; + echo 'pub: '.$_COOKIE['_PUBLIC_']."\n"; + echo 'priv: '.$_SESSION['_PRIVATE_']."\n"; + } + + /* (5) Granted */ + return $valid_parent !== false; + + } + + + /* (4) Checks a child + * + * @return status TRUE: Valid child + * FALSE: Invalid token + * + ---------------------------------------------------------*/ + private function check_child(){ + + /* (1) Check the parent + ---------------------------------------------------------*/ + /* (1) Process parent check */ + $valid_parent = $this->check_parent(); + + /* (2) Manage missing OR invalid parent */ + if( $valid_parent !== true ) + return false; + + /* (2) Check system state + ---------------------------------------------------------*/ + $has_token = isset($_SERVER['PHP_AUTH_DIGEST']) && preg_match( '/^[a-z0-9]{128}$/i', $_SERVER['PHP_AUTH_DIGEST'] ); + + /* (1) If no token -> false */ + if( !$has_token ) + return false; + + /* (3) Check child token + ---------------------------------------------------------*/ + /* (1) If no more steps -> false */ + if( $this->step <= 1 ){ + + if( self::$DEBUG ) + echo "/!\ no more token available\n"; + + return false; + + } + + if( self::$DEBUG ){ + echo "* CHILD RECEIVED *\n"; + echo 'sess_id: '.session_id()."\n"; + echo 'token: '.$_SERVER['PHP_AUTH_DIGEST']."\n"; + echo 'priv: '.$this->secret.'.'.$this->step."\n"; + } + + /* (2) Check child token */ + if( self::tgen($this->secret, $this->step) !== $_SERVER['PHP_AUTH_DIGEST'] ){ + + if( self::$DEBUG ) + echo "/!\ invalid child token\n"; + + return false; + + } + + /* (3) If valid token */ + if( self::$DEBUG ) + echo "[ Valid child token ]\n"; + + return true; + + } + + + + /* (5) Updates a child + * + * @inName inDesc + * + * @return outName outDesc + * + ---------------------------------------------------------*/ + public function init_child(){ + /* (1) Check child + ---------------------------------------------------------*/ + /* (1) Process check */ + $valid_child = $this->check_child(); + + /* (2) If invalid child -> destroy session */ + if( $valid_child !== true ) + return false; + + + /* (2) Update parent for other children + ---------------------------------------------------------*/ + /* (1) Decrement step */ + $this->step--; + + if( $this->step > 1 ){ // only if it is not the last step + + /* (2) Update step in session */ + $_SESSION['_PRIVATE_'] = $this->secret.'.'.$this->step; + + /* (3) Generate child-specific PUBLIC */ + header('Authorization: '.self::tgen($this->secret, $this->step)); + + if( self::$DEBUG ){ + echo "\n* CHILD UPDATED *\n"; + echo 'sess_id: '.session_id()."\n"; + echo 'token: '.$_SERVER['PHP_AUTH_DIGEST']."\n"; + echo 'priv: '.$_SESSION['_PRIVATE_']."\n"; + } + + } + + /* (2) Granted */ + return true; + + } + + + + /* (x) Generates a pseudo-rdm token + * + * @data Seed to use + * @depth Token depth + * + * @return token @data hashed @depth times + * + ---------------------------------------------------------*/ + private static function tgen($data, $depth=1){ + /* (0) If depth < 1 -> depth=1 */ + $depth = ( $depth < 1 ) ? 1 : $depth; + + /* (1) Apply salt */ + $hash = self::$salt.$data; + + /* (2) Hash @depth times */ + for( $d = 0 ; $d < $depth ; $d++ ) + $hash = ( $d == $depth-1 ) ? hash('sha512', $hash.self::$pepper) : hash('sha512', $hash); + + /* (3) Return hash */ + return $hash; + } + + + } \ No newline at end of file