├── .gitignore ├── .htaccess ├── .php_cs.php ├── LICENSE ├── README.md ├── TODO.md ├── bin └── composer ├── clients └── APIClient.php ├── composer.json ├── composer.lock ├── config.php ├── cover.png ├── docs.php ├── includes ├── classes │ ├── API.php │ ├── Auth.php │ ├── DatabaseErrors.php │ ├── Docs.php │ ├── Dump.php │ ├── Hooks.php │ ├── Logger.php │ ├── Request.php │ └── Response.php ├── compatibility.php ├── functions.php ├── loader.php └── template │ ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.min.css │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ └── main.css │ ├── footer.php │ ├── header.php │ ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png │ └── js │ ├── main.js │ └── vendor │ ├── bootstrap.bundle.js │ ├── bootstrap.bundle.min.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-3.2.1.min.js │ └── modernizr-2.6.1-respond-1.1.0.min.js ├── index.php ├── logs.php ├── logs └── .empty ├── plugins ├── actions.php ├── custom │ ├── .empty │ └── example.hooks.php ├── filters.php ├── functions.php ├── helpers │ └── example.hooks.php ├── loader.php └── tables │ ├── .empty │ └── example.php ├── renovate.json └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | **.logs 2 | **.log 3 | .idea/** 4 | vendor 5 | ghooks.lock -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | # Database Web API 2 | # @author Marco Cesarato 3 | # @copyright Copyright (c) 2018 4 | # @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 5 | # @link https://github.com/marcocesarato/Database-Web-API 6 | 7 | 8 | 9 | RewriteEngine On 10 | Options +FollowSymlinks 11 | Options +SymLinksIfOwnerMatch 12 | Options -MultiViews 13 | 14 | RewriteBase / 15 | 16 | RewriteCond $0#%{REQUEST_URI} ([^#]*)#(.*)\1$ 17 | RewriteRule ^.*$ - [E=CWD:%2] 18 | 19 | # Deny some methods 20 | RewriteCond %{REQUEST_METHOD} ^(TRACE|OPTIONS) 21 | RewriteRule .* – [L,F] 22 | 23 | RewriteRule .*\.(git|svn|hg).* %{ENV:CWD}index.php [L] # Deny access repo folder 24 | RewriteRule ^(config|composer|docs|.*\.([Hh][Tt][Aa])).* %{ENV:CWD}index.php [L] # Deny access 25 | RewriteRule ^(vendor|hooks|plugins|logs|screenshot|screenshots|screens|docs|documentation|clients)/((.*)\.php)?$ %{ENV:CWD}index.php [L] # Forbidden 26 | 27 | RewriteCond %{REQUEST_FILENAME} !-f 28 | RewriteCond %{REQUEST_FILENAME} !-d 29 | RewriteRule . %{ENV:CWD}index.php [L,QSA] 30 | 31 | 32 | 33 | # File protection 34 | 35 | Order Allow,Deny 36 | Deny from all 37 | 38 | 39 | Order Allow,Deny 40 | Deny from all 41 | 42 | 43 | Order Allow,Deny 44 | Deny from all 45 | Satisfy all 46 | 47 | 48 | Order Allow,Deny 49 | Deny from All 50 | 51 | 52 | Order Allow,Deny 53 | Deny from All 54 | -------------------------------------------------------------------------------- /.php_cs.php: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->name('*.php') 6 | ->ignoreDotFiles(true) 7 | ->ignoreVCS(true); 8 | 9 | $config = new PhpCsFixer\Config(); 10 | 11 | return $config 12 | ->setUsingCache(true) 13 | ->setRiskyAllowed(true) 14 | ->setCacheFile(__DIR__ . '/.php_cs.cache') 15 | ->setRules(array( 16 | '@PSR1' => true, 17 | '@PSR2' => true, 18 | '@Symfony' => true, 19 | 'psr4' => true, 20 | // Custom rules 21 | 'align_multiline_comment' => array('comment_type' => 'phpdocs_only'), // PSR-5 22 | 'phpdoc_to_comment' => false, 23 | 'array_indentation' => true, 24 | 'array_syntax' => array('syntax' => 'short'), 25 | 'cast_spaces' => array('space' => 'none'), 26 | 'concat_space' => array('spacing' => 'one'), 27 | 'compact_nullable_typehint' => true, 28 | 'declare_equal_normalize' => array('space' => 'single'), 29 | 'increment_style' => array('style' => 'post'), 30 | 'list_syntax' => array('syntax' => 'long'), 31 | 'no_short_echo_tag' => true, 32 | 'phpdoc_align' => false, 33 | 'phpdoc_no_empty_return' => false, 34 | 'phpdoc_order' => true, // PSR-5 35 | 'phpdoc_no_useless_inheritdoc' => false, 36 | 'protected_to_private' => false, 37 | 'yoda_style' => false, 38 | 'method_argument_space' => array('on_multiline' => 'ensure_fully_multiline'), 39 | 'ordered_imports' => array( 40 | 'sort_algorithm' => 'alpha', 41 | 'imports_order' => array('class', 'const', 'function'), 42 | ), 43 | )) 44 | ->setFinder($finder); 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Database Web API 2 | ![](cover.png) 3 | 4 | **Version:** 0.7.0 5 | 6 | **Github:** https://github.com/marcocesarato/Database-Web-API 7 | 8 | **Author:** Marco Cesarato 9 | 10 | ## Description 11 | Dynamically generate RESTful APIs from the contents of a database table. Provides JSON, XML, and HTML. Supports most popular databases. 12 | 13 | ## What problem this solves 14 | Creating an API to access information within existing database tables is laborious task, when done as a bespoke task. This is often dealt with by exporting the contents of the database as CSV files, and providing downloads of them as a “good enough” solution. 15 | 16 | ## How this solves it 17 | Database Web API acts as a filter, sitting between a database and the browser, allowing users to interact with that database as if it was a native API. The column names function as the key names. This obviates the need for custom code for each database layer. 18 | 19 | When Alternative PHP Cache (APC) is installed, parsed data is stored within APC, which accelerates its functionality substantially. While APC is not required, it is recommended highly. 20 | 21 | ## Wiki 22 | 23 | Go to this link for go to the wiki of the platform and have a better informations about the usage of this platform and have more examples: 24 | 25 | https://github.com/marcocesarato/Database-Web-API/wiki 26 | 27 | ## Databases supported 28 | * 4D 29 | * CUBRID 30 | * Firebird/Interbase 31 | * IBM 32 | * Informix 33 | * MS SQL Server 34 | * MySQL 35 | * ODBC and DB2 36 | * Oracle 37 | * PostgreSQL 38 | * SQLite 39 | 40 | ### Requirements 41 | * PHP 42 | * Database 43 | * APC (optional) 44 | 45 | ## Installation 46 | * Set the configuration on `config.php`. Follow the below example to register a new dataset. Tip: It's best to provide read-only database credentials here if you want read only. 47 | * _(Optional)_ If you want enable an authentication system you must compile on the `config.php` the constant `__API_AUTH__` as on the example below. 48 | * _(Optional)_ Document the API. For this you can use auto-documentation using file `docs.php` 49 | * _(Optional)_ Use Hooks for manage permissions (as `can_read`, `can_write`, `can_edit`, `can_delete`) 50 | 51 | ## Configuration 52 | Edit `config.php` to include a single instance of the following for each dataset (including as many instances as you have datasets): 53 | 54 | ### Define API Name 55 | ```php 56 | define("__API_NAME__", "Database Web API"); // API Name 57 | ``` 58 | 59 | ### Define datasets 60 | 61 | **Dataset configurations:** 62 | 63 | | Settings | Description | Default | 64 | |------------------|------------------------------------------------------------------------------------|-----------| 65 | | default | Default dataset | false | 66 | | api | Accessible through API | true | 67 | | name | Database Name | | 68 | | username | Database Username | root | 69 | | password | Database Password | root | 70 | | server | Database Server Address | localhost | 71 | | ttl | Cache time to live (set 1 for disable) | 3600 | 72 | | port | Database Port | 3306 | 73 | | type | Database Type (ex. `mysql`, `psql` ecc..) | mysql | 74 | | table_list | Database Tables Whitelist (Allow only the tables in this list, if empty allow all) | null | 75 | | table_blacklist | Database Tables Blacklist | null | 76 | | table_docs | Database Documentation (ex. /dataset/docs/table.html) | array() | 77 | | column_list | Database Columns's whitelist (Allow only the columns in this list, if empty allow all) | null | 78 | | column_blacklist | Database Columns's blacklist | null | 79 | 80 | #### Example complete with explanation 81 | ```php 82 | define("__API_DATASETS__", serialize(array( 83 | 'dataset' => array( 84 | 'default' => true, // Default dataset 85 | 'api' => true, // Accessible from API request url (ex. is false if you have a different database for auth users) 86 | 'name' => 'database_name', // Database name 87 | 'username' => 'user', // root is default 88 | 'password' => 'passwd', // root is default 89 | 'ttl' => 1, // Cache time to live. Disable cache (1 second only) 90 | 'server' => 'localhost', // localhost default 91 | 'port' => 5432, // 3306 is default 92 | 'type' => 'pgsql', // mysql is default 93 | 'table_list' => array( // Tables's whitelist (Allow only the tables in this list, if empty allow all) 94 | 'users' 95 | ), 96 | 'table_blacklist' => array( // Tables's blacklist 97 | 'passwords' 98 | ), 99 | 'table_docs' => array( 100 | /** @example 101 | 'table' => array( 102 | "column" => array( 103 | "description" => "Column description", 104 | "example" => "1", 105 | ), 106 | ), 107 | */ 108 | ), // For Autodocoumentation, url ex. /dataset/docs/table.html 109 | 'column_list' => array( // Columns's whitelist (Allow only the columns in this list, if empty allow all) 110 | 'users' => array( 111 | 'username', 112 | 'name', 113 | 'surname' 114 | ) 115 | ), 116 | 'column_blacklist' => array( // Columns's blacklist 117 | 'users' => array( 118 | 'password', 119 | ) 120 | ), 121 | ), 122 | ))); 123 | ``` 124 | 125 | **Note:** All fields of `__API_DATASETS__` (except the name of database) are optional and will default to the above. 126 | 127 | #### Example 128 | 129 | Here is a dataset example for a MySQL database named “inspections,” accessed with a MySQL user named “website” and a password of “s3cr3tpa55w0rd,” with MySQL running on the same server as the website, with the standard port of 3306. All tables may be accessed by Database to API except for “cache” and “passwords,” and among the accessible tables, the “password_hint” column may not be accessed via Database to API. All of this is registered to create an API named “facility-inspections”. 130 | 131 | ```php 132 | array( 133 | 'default' => true, 134 | 'name' => 'inspections', 135 | 'username' => 'website', 136 | 'password' => 's3cr3tpa55w0rd', 137 | 'server' => 'localhost', 138 | 'ttl' => 1, 139 | 'port' => 3306, 140 | 'type' => 'mysql', 141 | 'table_docs' => array(), 142 | 'table_list' => array(), 143 | 'table_blacklist' => array('cache', 'passwords'), 144 | 'column_blacklist' => array('password_hint'), 145 | 'column_list' => array(), 146 | ); 147 | ``` 148 | 149 | Retrieving the contents of the table history within this dataset as JSON would be accomplished with a request for /facility-inspections/history.json. Note that it is the name of the dataset (facility-inspections) and not the name of the database (inspections) that is specified in the URL. 150 | 151 | For a SQLite database, simply provide the path to the database in name. 152 | 153 | For an Oracle database, you can either specify a service defined in tsnames.ora (e.g. dept_spending) or you can define an Oracle Instant Client connection string (e.g., //localhost:1521/dept_spending). 154 | 155 | 156 | 157 | ### _(Optional)_ Authentication system 158 | 159 | #### Auth configuration 160 | 161 | | Setting | Description | Type | 162 | |-----------------|-------------------------------------------------------------------|-------| 163 | | sqlite | When enabled store token on SQLite file | Bool | 164 | | sqlite_database | SQLite filename (only if sqlite = true) | Text | 165 | | api_database | Set database name where create api_table (Only if sqlite = false) (`__DATASET__`) | Text | 166 | | api_table | Set database table name where store tokens | Text | 167 | | users | Users table to validate | Array | 168 | 169 | 170 | ##### Users configuration 171 | 172 | | Setting | Description | Type | 173 | |----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------| 174 | | database | Database where users are stored (`__DATASET__`) | Bool | 175 | | table | Users table name | Text | 176 | | columns | 'id' => 'user_id' // Id column name
'username' => 'user_name' // Username column name
'password' => 'password' // Password column name
'admin' => array('is_admin' => 1) // Admin condition (can be null) | Array | 177 | | search | Search condition | Text | 178 | | check | Validation users condition (ex. is_active = 1) (can be null) | Text | 179 | 180 | ```php 181 | define("__API_AUTH__", serialize(array( // Set null for disable authentication 182 | 'sqlite' => false, // Enabled save token on SQLite file 183 | 'sqlite_database' => 'api_token', // SQLite filename (only with sqlite = true) 184 | 'api_database' => 'dataset', // Authentication database 185 | 'api_table' => 'api_authentications', // API token table name 186 | 'users' => array( 187 | 'database' => 'dataset', // Database where users are stored 188 | 'table' => 'users', // Table where users are stored 189 | 'columns' => array( 190 | 'id' => 'user_id', // Id column name 191 | 'username' => 'user_name', // Username column name 192 | 'password' => 'password', // Password column name 193 | 'admin' => array('is_admin' => 1) // Admin bypass condition. With this condition true API bypass all black/whitelists and permissions. Set NULL for disable 194 | ), 195 | 'search' => array('user_id', 'email', 'username'), // Search user by these fields 196 | 'check' => array('active' => 1) // Some validation checks. In this case if the column 'active' with value '1'. Set NULL for disable 197 | ), 198 | ))); 199 | ``` 200 | 201 | ## API Structure 202 | 203 | ### Format availables: 204 | 205 | - JSON 206 | 207 | - XML 208 | 209 | - HTML 210 | 211 | 212 | ### Generic URL format for all kind of request: 213 | 214 | #### Standard 215 | 216 | * Fetch all: `/[database]/[table].[format]` 217 | * Fetch all with limit: `/[database]/[limit]/[table].[format]` 218 | * Fetch: `/[database]/[table]/[ID].[format]` 219 | * Fetch search by column: `/[database]/[table]/[column]/[value].[format]` 220 | * Documentation: `/[database]/docs/[table].[format]` 221 | 222 | 223 | ## Authentication 224 | 225 | Authentication needed for browse the database. 226 | 227 | The authentication permit to managed the privilege of the users (read, write, modify, delete) 228 | 229 | - Authentication: `/auth.[format]` 230 | 231 | Set the header **Auth-Account** with the username/user id and **Auth-Password** with the encrypted password like this: 232 | 233 | **Request example:** 234 | 235 | ```http 236 | GET /auth.json HTTP/1.1 237 | Host: localhost 238 | Auth-Account: marco.cesarato 239 | Auth-Password: md5password 240 | ``` 241 | 242 | **Response example:** 243 | 244 | ```json 245 | [{"token": "b279fb1d0708ed81e7a194e0c5d928b6"}] 246 | ``` 247 | 248 | **Example of token usage on GET, POST, PUT, PATCH and DELETE requests:** 249 | 250 | Set the header **Access-Token** with the token values received from auth request like this: 251 | 252 | ```http 253 | GET /database/users.json` HTTP/1.1 254 | Host: localhost 255 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 256 | ``` 257 | 258 | 259 | ## Check Authentication 260 | 261 | Check authentication check is needed for verify if a token is valid. 262 | 263 | - Check authentication: `/auth/check.[format]` 264 | 265 | **Request example:** 266 | 267 | ```http 268 | GET /auth/check.json HTTP/1.1 269 | Host: localhost 270 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 271 | ``` 272 | 273 | **Response example:** 274 | 275 | ```json 276 | { 277 | 278 | "user": { 279 | "id": "1", 280 | "role_id": "", 281 | "is_admin": true 282 | }, 283 | "response": { 284 | "status": 200, 285 | "message": "OK" 286 | } 287 | 288 | } 289 | ``` 290 | 291 | 292 | ## GET Request 293 | 294 | Retrieve data from dataset 295 | 296 | - Fetch all: `/[database]/[table].[format]` 297 | 298 | - Fetch all with limit: `/[database]/[limit]/[table].[format]` 299 | 300 | - Fetch: `/[database]/[table]/[ID].[format]` 301 | 302 | - Fetch search by column: `/[database]/[table]/[column]/[value].[format]` 303 | 304 | - Fetch all joining table: 305 | 306 | ```js 307 | join[table] = array( 308 | 'on' => , // Column of the table joined 309 | 'value' => , // Column of main table or value 310 | 'method' => (left|inner|right) // Optional 311 | ) 312 | ``` 313 | 314 | **Example with value:** 315 | 316 | ```js 317 | join[users]['on'] = id 318 | join[users]['value'] = 1 319 | join[users]['method'] = 'INNER' 320 | ``` 321 | 322 | **Example with column:** 323 | 324 | ```js 325 | join[users]['on'] = id // Column of the table joined 326 | join[users]['value'] = user_id // Column of the main table (no users) 327 | join[users]['method'] = 'INNER' 328 | ``` 329 | 330 | - Additional parameters 331 | 332 | ex: `/[database]/[table]/[column]/[value].[format]?order_by=[column]&direction=[direction]` 333 | 334 | **Examples of GET requests:** 335 | 336 | ```http 337 | GET /dataset/users.json HTTP/1.1 338 | Host: localhost 339 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 340 | ``` 341 | 342 | ```http 343 | GET /dataset/10/users.json HTTP/1.1 344 | Host: localhost 345 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 346 | ``` 347 | 348 | ```http 349 | GET /dataset/users/1.json HTTP/1.1 350 | Host: localhost 351 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 352 | ``` 353 | 354 | ```http 355 | GET /dataset/users/is_active/1.json?order_by=username&direction=desc HTTP/1.1 356 | Host: localhost 357 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 358 | ``` 359 | 360 | ### Advanced search 361 | 362 | **Note:** These examples are valid only for **GET** and **PUT** requests 363 | 364 | Search single value 365 | 366 | ```php 367 | where[column] = 1 // column = 1 368 | where[column][=] = 1 // column = 1 369 | where[column][!] = 1 // column != 1 370 | where[column][>] = 1 // column > 1 371 | where[column][<] = 1 // column < 1 372 | where[column][>=] = 1 // column >= 1 373 | where[column][<=] = 1 // column <= 1 374 | where[column][%] = "%1" // column LIKE "%1" 375 | ``` 376 | 377 | Search multiple values 378 | 379 | ```php 380 | where[column] = array(1,5,7) // IN (...) (IN can be equal to an OR) 381 | where[column][=] = array(1,5,7) // IN (...) 382 | where[column][!] = array(1,5,7) // NOT IN (...) 383 | where[column][>] = array(1,2) // column > 1 OR column > 2 384 | where[column][<] = array(1,2) // column < 1 OR column < 2 385 | where[column][>=] = array(1,2) // column >= 1 OR column >= 2 386 | where[column][<=] = array(1,2) // column <= 1 OR column <= 2 387 | where[column][%] = array("%1","%2") // column LIKE "%1" OR column LIKE "%2" 388 | ``` 389 | 390 | Specify column's table 391 | 392 | ```php 393 | where['table.column'][=] = array(1,5,7) 394 | ``` 395 | 396 | Compare between two different table columns 397 | 398 | ```php 399 | where['table_a.column_a'] = 'table_b.column_b' 400 | ``` 401 | 402 | Compare between different columns of main table 403 | 404 | ```php 405 | where['column_a'] = 'table_a.column_b' 406 | // OR 407 | where['table_a.column_a'] = 'table_a.column_b' 408 | 409 | // WRONG 410 | where['column_a'] = 'column_b' 411 | ``` 412 | 413 | ### Additional parameters 414 | 415 | - `order_by`: column_name 416 | 417 | Can be and array or a string 418 | 419 | ```php 420 | order_by = 'username, name, surname' 421 | // OR 422 | order_by = array('username', 'name', 'surname') 423 | ``` 424 | 425 | for more specific order direction 426 | 427 | ```php 428 | order_by['users.username'] = 'DESC' 429 | ``` 430 | 431 | for cast a specific type 432 | 433 | ```php 434 | order_by['users.username::varchar'] = 'DESC' 435 | order_by['users.id::int'] = 'DESC' 436 | ``` 437 | 438 | - `direction`: `ASC` or `DESC` (default `ASC`) 439 | 440 | - `limit`: max elements to retrieve 441 | 442 | ex: `/[database]/[tabel]/[colomn]/[value].[format]?order_by=[column]&direction=[direction]` 443 | 444 | ### Documentation 445 | 446 | *PS:* Work only with pgsql and mysql database type at the moment 447 | 448 | For get auto-documentation of a database table: 449 | 450 | - Documentation index URL format : `/[database]/docs/index.[format]` 451 | - Documentation URL format: `/[database]/docs/[table].[format]` 452 | 453 | For have a separated file where document your database you can use `/docs.php` 454 | 455 | 456 | 457 | ## POST Request 458 | 459 | Insert data 460 | 461 | **Single insert:** 462 | 463 | - Select the table on URL: `/[database]/[table].[format]` 464 | - Insert parameter: `insert[] = ` 465 | 466 | **Multiple insert:** 467 | 468 | - Select dataset on URL: `/[database].[format]` 469 | - Insert parameter: `insert[][] = ` 470 | 471 | **Note**: At the moment you can add only one row for table 472 | 473 | **Examples of POST requests:** 474 | 475 | **Single insert:** 476 | 477 | ```http 478 | POST /dataset/users.json HTTP/1.1 479 | Host: localhost 480 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 481 | 482 | insert[username]=Marco&insert[email]=cesarato.developer@gmail.com&insert[password]=3vwjehvdfjhefejjvw&insert[is_active]=1 483 | ``` 484 | 485 | **Multiple insert:** 486 | 487 | ```http 488 | POST /dataset.json HTTP/1.1 489 | Host: localhost 490 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 491 | 492 | insert[users][username]=Marco&insert[users][email]=cesarato.developer@gmail.com&insert[users][password]=3vwjehvdfjhefejjvw&insert[users][is_active]=1 493 | ``` 494 | 495 | ```http 496 | POST /dataset.json HTTP/1.1 497 | Host: localhost 498 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 499 | 500 | insert[users][0][username]=Marco&insert[users][0][email]=cesarato.developer@gmail.com&insert[users][0][password]=3vwjehvdfjhefejjvw&insert[users][0][is_active]=1&insert[users][1][username]=Brad&insert[users][1][email]=brad@gmail.com&insert[users][1][password]=erwerwerffweeqewrf&insert[users][1][is_active]=1 501 | ``` 502 | 503 | ## PATCH/PUT Request 504 | 505 | Update data 506 | 507 | **Single update:** 508 | 509 | - Select the row on URL: `/[database]/[table]/[id].[format]` 510 | - Update parameter: `update[] = ` 511 | 512 | **Multiple update:** 513 | 514 | - Select the dataset on URL: `/[database].[format]` 515 | - Update parameter: `update[
][values][] = ` 516 | - Multiple update parameter conditions: `update[
][where][] = ` 517 | 518 | **Note**: At the moment you can update only one row for table 519 | 520 | **Examples of PUT Requests:** 521 | 522 | **Single Update:** 523 | 524 | ```http 525 | PUT /dataset/users/1.json HTTP/1.1 526 | Host: localhost 527 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 528 | 529 | update['username']=Marco&update['email']=cesarato.developer@gmail.com&update['password']=3vwjehvdfjhefejjvw&update['is_active']=1 530 | ``` 531 | 532 | **Multi-table Update:** 533 | 534 | ```http 535 | PUT /dataset.json HTTP/1.1 536 | Host: localhost 537 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 538 | 539 | update[users][values][username]=Marco&update[users][values][email]=cesarato.developer@gmail.com&update[users][where][id]=1&update[cities][values][name]=Padova&update[cities][where][id]=1 540 | ``` 541 | 542 | **Multiple Update:** 543 | 544 | ```http 545 | PUT /dataset.json HTTP/1.1 546 | Host: localhost 547 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 548 | 549 | update[users][][values][username]=Marco&update[users][][values][email]=cesarato.developer@gmail.com&update[users][][where][id]=1&update[cities][][values][name]=Padova&update[cities][][where][id]=1&update[cities][][values][name]=Milano&update[cities][][where][id]=2 550 | ``` 551 | 552 | 553 | 554 | ## DELETE Request 555 | 556 | Delete data 557 | 558 | - Select the row on table: `/[database]/[table]/[id].[format]` 559 | 560 | **Examples of DELETE Requests:** 561 | 562 | ```http 563 | DELETE /dataset/users/1.json HTTP/1.1 564 | Host: localhost 565 | Access-Token: b279fb1d0708ed81e7a194e0c5d928b6 566 | ``` 567 | 568 | ## Hooks 569 | 570 | For write hooks you can use `plugins/custom` folder or edit manually the examples on `plugins/actions.php` or `plugins/filters.php` 571 | 572 | Remember to name file like this pattern: `[FILENAME].hooks.php` or it will not be included automatically (else you can include it manually) 573 | 574 | 575 | ### Tips 576 | 577 | You can use this code for have a database instance and the current user authenticated row: 578 | 579 | ```php 580 | $user = Auth::getUser(); // User row 581 | $db = API::getDatabase('dataset'); // You can specify dataset. Return PDO Object 582 | ``` 583 | 584 | ### Hooks list 585 | 586 | https://github.com/marcocesarato/Database-Web-API/wiki/3.2)-Hooks:-List 587 | 588 | ### Most important hooks 589 | 590 | | Hook | Type | Description | Params | Return | 591 | |-----------------------|--------|-----------------------------------------------------------------|------------------------------------------------------------|--------| 592 | | sql_restriction | Filter | Add restriction on where conditions for each query | (string) $restriction (string) $table (string) $permission | String | 593 | | can_read | Filter | Return if can get/select | (bool) $permission = true | Bool | 594 | | can_write | Filter | Return if can post/insert | (bool) $permission = true | Bool | 595 | | can_edit | Filter | Return if can put/update | (bool) $permission = true | Bool | 596 | | can_delete | Filter | Return if can delete | (bool) $permission = true | Bool | 597 | | on_read | Filter | Result content returned on get/read | (array) $data (string) $table | Array | 598 | | on_write | Filter | Result content returned on post/write | (array) $data (string) $table | Array | 599 | | on_edit | Filter | Result content returned on put/edit | (array) $data (string) $table | Array | 600 | | on_delete | Filter | Get result content returned on delete | (array) $data (string) $table | Array | 601 | | render | Filter | Manipulate data response rendered | (array) $data (string) $query (string) $method | Array | 602 | 603 | ### Hooks detail 604 | * Filter: `sql_restriction` 605 | 606 | **Options of *$permission*:** 607 | 608 | ```php 609 | case 'READ': 610 | case 'EDIT': 611 | case 'DELETE': 612 | ``` 613 | **Return** 614 | 615 | ```php 616 | // All denied 617 | $sql = "'1' = '0'"; 618 | // All allowed 619 | $sql = "'1' = '1'"; 620 | ``` 621 | **Examples:** 622 | 623 | ```php 624 | // Only Created 625 | $sql = 'created_by = '.$user['id']; 626 | // Only Team 627 | $sql = 'created_by IN ('.implode(',',$teams_ids).')'; 628 | ``` 629 | 630 | ## Clients 631 | 632 | ### PHP Client 633 | 634 | **Filename:** `apiclient.class.php` 635 | 636 | **Class name:** APIClient 637 | 638 | | Method | Description | Type | Parameters | Return | 639 | | ---------------- | -------------------------------------------------- | ----------------- | ---------------------------------- | ---------------------------------------------- | 640 | | getInstance | | public
static | | Returns static reference to the class instance | 641 | | isConnected | Is Connected | public | | bool | 642 | | setUrl | Set Url | public
static | string $url | | 643 | | setAccessToken | Set Access token | public
static | string $token | | 644 | | setDataset | Set Dataset | public
static | string $dataset | | 645 | | setTimeout | Set Timeout | public
static | int $timeout = 15 | | 646 | | setExecutionTime | Set max execution time | public
static | int $time = 60 | | 647 | | get | Get data | public | string $table
array $where | bool
mixed | 648 | | insert | Insert data | public | array $params | bool
mixed | 649 | | update | Update data | public | array $params | bool
mixed | 650 | | replace | Replace data | public | array $params | bool
mixed | 651 | | delete | Delete data | public | string $table
array $params | bool
mixed | 652 | | searchElement | Search object in array | public | $array
$key
$value | mixed | 653 | | filterBy | Filter object in array | public | $key
$value
$array
$limit | mixed | 654 | | filter | Filter object in array | public | $values
$array
$limit | mixed | 655 | 656 | 657 | ## Credits 658 | 659 | https://github.com/project-open-data/db-to-api 660 | 661 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # PHP Database Web API - TODO 2 | 3 | - Table and columns alias 4 | - Hooks CheckColumn and CheckTable 5 | - Implementeation of OAuth 2.0 6 | - Mini admin page for permissions and docs and maybe Users 7 | -------------------------------------------------------------------------------- /bin/composer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/bin/composer -------------------------------------------------------------------------------- /clients/APIClient.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class APIClient 14 | { 15 | // Public 16 | private static $DEBUG = false; 17 | private static $URL = ''; 18 | private static $DATASET = ''; 19 | private static $ACCESS_TOKEN = ''; 20 | private static $TIMEOUT = 15; 21 | private static $EXECUTION_TIME = 60; 22 | private static $MEMORY_LIMIT = '1G'; 23 | 24 | // Protected 25 | protected static $instance = null; 26 | 27 | // Private 28 | private static $_DATA = []; 29 | 30 | /** 31 | * Singleton constructor. 32 | */ 33 | protected function __construct() 34 | { 35 | // Depends from data weight 36 | ini_set('memory_limit', self::$MEMORY_LIMIT); 37 | self::setExecutionTime(self::$EXECUTION_TIME); 38 | } 39 | 40 | /** 41 | * Returns static reference to the class instance. 42 | */ 43 | public static function getInstance() 44 | { 45 | if (null === self::$instance) { 46 | self::$instance = new static(); 47 | } 48 | 49 | return self::$instance; 50 | } 51 | 52 | /** 53 | * Is Connected. 54 | * 55 | * @return bool 56 | */ 57 | public function isConnected() 58 | { 59 | if (empty(self::$ACCESS_TOKEN) || empty(self::$URL)) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Set Url. 68 | * 69 | * @param $url 70 | */ 71 | public static function setUrl($url) 72 | { 73 | self::$URL = $url; 74 | } 75 | 76 | /** 77 | * Get Url. 78 | * 79 | * @param string $page 80 | * 81 | * @return string 82 | */ 83 | private static function getUrl($page = '') 84 | { 85 | if (empty($page)) { 86 | $page = '.json'; 87 | } else { 88 | $page = '/' . ltrim($page, '/'); 89 | } 90 | $URL = str_replace('\\', '/', self::$URL); 91 | 92 | return rtrim($URL, '/') . '/api/' . self::$DATASET . $page; 93 | } 94 | 95 | /** 96 | * Set Access token. 97 | * 98 | * @param $token 99 | */ 100 | public static function setAccessToken($token) 101 | { 102 | self::$ACCESS_TOKEN = $token; 103 | } 104 | 105 | /** 106 | * Set Dataset. 107 | * 108 | * @param $dataset 109 | */ 110 | public static function setDataset($dataset) 111 | { 112 | self::$DATASET = $dataset; 113 | } 114 | 115 | /** 116 | * Set Timeout. 117 | * 118 | * @param $timeout 119 | */ 120 | public static function setTimeout($timeout = 15) 121 | { 122 | self::$TIMEOUT = $timeout; 123 | } 124 | 125 | /** 126 | * Set max execution time. 127 | * 128 | * @param $time 129 | */ 130 | public static function setExecutionTime($time = 60) 131 | { 132 | self::$EXECUTION_TIME = $time; 133 | ini_set('max_execution_time', self::$EXECUTION_TIME); 134 | set_time_limit(self::$EXECUTION_TIME); 135 | } 136 | 137 | /** 138 | * Build url query params 139 | * as http_build_query build a query url the difference is 140 | * that this function is array recursive and compatible with PHP4. 141 | * 142 | * @param $query 143 | * @param string $parent 144 | * 145 | * @return string 146 | * 147 | * @author Marco Cesarato 148 | */ 149 | private static function buildQuery($query, $parent = null) 150 | { 151 | $query_array = []; 152 | foreach ($query as $key => $value) { 153 | $_key = empty($parent) ? urlencode($key) : $parent . '[' . urlencode($key) . ']'; 154 | if (is_array($value)) { 155 | $query_array[] = self::buildQuery($value, $_key); 156 | } else { 157 | $query_array[] = $_key . '=' . urlencode($value); 158 | } 159 | } 160 | 161 | return implode('&', $query_array); 162 | } 163 | 164 | /** 165 | * HTTP Request. 166 | * 167 | * @param $url 168 | * 169 | * @return mixed 170 | */ 171 | private static function doRequest($url, $body = null, $method = 'GET') 172 | { 173 | $options = [ 174 | CURLOPT_RETURNTRANSFER => true, // return web page 175 | CURLOPT_HEADER => true, // return headers in addition to content 176 | CURLOPT_FOLLOWLOCATION => true, // follow redirects 177 | CURLOPT_ENCODING => '', // handle all encodings 178 | CURLOPT_AUTOREFERER => true, // set referer on redirect 179 | CURLOPT_CONNECTTIMEOUT => self::$TIMEOUT, // timeout on connect 180 | CURLOPT_TIMEOUT => self::$TIMEOUT, // timeout on response 181 | CURLOPT_MAXREDIRS => 10, // stop after 10 redirects 182 | CURLINFO_HEADER_OUT => true, 183 | CURLOPT_SSL_VERIFYPEER => false, // Validate SSL Cert 184 | CURLOPT_SSL_VERIFYHOST => false, // Validate SSL Cert 185 | CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, 186 | CURLOPT_HTTPHEADER => [ 187 | 'Accept-Language: ' . @$_SERVER['HTTP_ACCEPT_LANGUAGE'], 188 | 'Cache-Control: no-cache', 189 | 'Access-Token: ' . self::$ACCESS_TOKEN, 190 | ], 191 | CURLOPT_USERAGENT => @$_SERVER['HTTP_USER_AGENT'], 192 | CURLOPT_POSTFIELDS => (empty($body) ? null : $body), 193 | ]; 194 | 195 | if ($body !== false && !empty($body) && $method == 'GET') { 196 | $method = 'POST'; 197 | } 198 | 199 | $method = strtoupper($method); 200 | if (in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'])) { 201 | $options[CURLOPT_CUSTOMREQUEST] = $method; 202 | } 203 | 204 | $options = array_filter($options); 205 | 206 | $ch = curl_init($url); 207 | 208 | $rough_content = self::execRequest($ch, $options); 209 | $err = curl_errno($ch); 210 | $errmsg = curl_error($ch); 211 | $header = curl_getinfo($ch); 212 | curl_close($ch); 213 | 214 | $header_content = substr($rough_content, 0, $header['header_size']); 215 | $body_content = trim(str_replace($header_content, '', $rough_content)); 216 | 217 | $header['errno'] = $err; 218 | $header['errmsg'] = $errmsg; 219 | $header['headers'] = $header_content; 220 | $header['content'] = $body_content; 221 | 222 | return $header; 223 | } 224 | 225 | /** 226 | * Prevent 301/302/307 Code. 227 | * 228 | * @param resource $ch 229 | * @param array $options 230 | * 231 | * @return bool|string 232 | */ 233 | private static function execRequest($ch, $options = []) 234 | { 235 | $options[CURLOPT_FOLLOWLOCATION] = false; 236 | curl_setopt_array($ch, $options); 237 | 238 | $rough_content = curl_exec($ch); 239 | $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 240 | 241 | if ($http_code == 301 || $http_code == 302 || $http_code == 307) { 242 | preg_match('/(Location:|URI:)(.*?)\n/', $rough_content, $matches); 243 | if (isset($matches[2])) { 244 | $redirect_url = trim($matches[2]); 245 | if ($redirect_url !== '') { 246 | $options[CURLOPT_URL] = $redirect_url; 247 | 248 | return self::execRequest($ch, $options); 249 | } 250 | } 251 | } 252 | 253 | return $rough_content; 254 | } 255 | 256 | /** 257 | * Print debug message. 258 | * 259 | * @param $msg 260 | */ 261 | private function _debug($msg) 262 | { 263 | if (self::$DEBUG) { 264 | echo '
' . $msg . '
'; 265 | } 266 | } 267 | 268 | /** 269 | * Get data. 270 | * 271 | * @param $table 272 | * @param array $where 273 | * 274 | * @return bool|mixed 275 | */ 276 | public function get($table, $params = []) 277 | { 278 | if (!$this->isConnected()) { 279 | return false; 280 | } 281 | 282 | $param_key = serialize($params); 283 | 284 | if (!empty(self::$_DATA[$table][$param_key])) { 285 | return self::$_DATA[$table][$param_key]; 286 | } 287 | 288 | $params_query = !empty($params) ? self::buildQuery($params) : ''; 289 | $url = self::getUrl($table . '.json?' . $params_query); 290 | $request = self::doRequest($url); 291 | $this->_debug('APIClient GET: Sent GET REQUEST to ' . $url); 292 | 293 | self::$_DATA[$table][$param_key] = $request['content']; 294 | 295 | if ($request['errmsg']) { 296 | $this->_debug('APIClient GET: Error ' . $request['errno'] . ' => ' . $request['errmsg']); 297 | } 298 | 299 | self::$_DATA[$table][$param_key] = @json_decode(self::$_DATA[$table][$param_key]); 300 | 301 | if (empty(self::$_DATA[$table][$param_key])) { 302 | return false; 303 | } 304 | 305 | $this->_debug('APIClient GET: Count ' . count(self::$_DATA[$table][$param_key], true)); 306 | 307 | return self::$_DATA[$table][$param_key]; 308 | } 309 | 310 | /** 311 | * Insert data. 312 | * 313 | * @param array $params 314 | * 315 | * @return bool|mixed 316 | */ 317 | public function insert($params = []) 318 | { 319 | if (!$this->isConnected()) { 320 | return false; 321 | } 322 | 323 | $params_query = !empty($params) ? self::buildQuery($params) : ''; 324 | $url = self::getUrl(); 325 | $request = self::doRequest($url, $params_query); 326 | $this->_debug('APIClient INSERT: Sent POST REQUEST to ' . $url); 327 | //$this->_debug("APIClient INSERT: Params \r\n".var_export($params, true)); 328 | 329 | $this->_debug('APIClient INSERT: Params ' . var_export($params, true)); 330 | 331 | if ($request['errmsg']) { 332 | $this->_debug('APIClient INSERT: Error ' . $request['errno'] . ' => ' . $request['errmsg']); 333 | } 334 | 335 | $this->_debug('APIClient INSERT: Header ' . $request['headers']); 336 | 337 | $response = $request['content']; 338 | $response = @json_decode($response); 339 | 340 | if (empty($response)) { 341 | return false; 342 | } 343 | 344 | $this->_debug("APIClient INSERT: Response \r\n" . var_export($response, true)); 345 | 346 | return $response; 347 | } 348 | 349 | /** 350 | * Update data. 351 | * 352 | * @param array $params 353 | * 354 | * @return bool|mixed 355 | */ 356 | public function update($params = []) 357 | { 358 | if (!$this->isConnected()) { 359 | return false; 360 | } 361 | 362 | $params_query = !empty($params) ? self::buildQuery($params) : ''; 363 | $url = self::getUrl(); 364 | $request = self::doRequest($url, $params_query, 'PATCH'); 365 | $this->_debug('APIClient UPDATE: Sent PUT REQUEST to ' . $url); 366 | 367 | if ($request['errmsg']) { 368 | $this->_debug('APIClient UPDATE: Error ' . $request['errno'] . ' => ' . $request['errmsg']); 369 | } 370 | 371 | $response = $request['content']; 372 | $response = @json_decode($response); 373 | 374 | if (empty($response)) { 375 | return false; 376 | } 377 | 378 | $this->_debug("APIClient UPDATE: Response \r\n" . var_export($request, true)); 379 | 380 | return $response; 381 | } 382 | 383 | /** 384 | * Replace data. 385 | * 386 | * @param array $params 387 | * 388 | * @return bool|mixed 389 | */ 390 | public function replace($params = []) 391 | { 392 | if (!$this->isConnected()) { 393 | return false; 394 | } 395 | 396 | $params_query = !empty($params) ? self::buildQuery($params) : ''; 397 | $url = self::getUrl(); 398 | $request = self::doRequest($url, $params_query, 'PUT'); 399 | $this->_debug('APIClient REPLACE: Sent PUT REQUEST to ' . $url); 400 | 401 | if ($request['errmsg']) { 402 | $this->_debug('APIClient REPLACE: Error ' . $request['errno'] . ' => ' . $request['errmsg']); 403 | } 404 | 405 | $response = $request['content']; 406 | $response = @json_decode($response); 407 | 408 | if (empty($response)) { 409 | return false; 410 | } 411 | 412 | $this->_debug("APIClient REPLACE: Response \r\n" . var_export($request, true)); 413 | 414 | return $response; 415 | } 416 | 417 | /** 418 | * Delete data. 419 | * 420 | * @param $table 421 | * @param array $params 422 | * 423 | * @return bool|mixed 424 | */ 425 | public function delete($table, $params = []) 426 | { 427 | if (!$this->isConnected()) { 428 | return false; 429 | } 430 | 431 | $params_query = !empty($params) ? self::buildQuery($params) : ''; 432 | $url = self::getUrl($table . '.json?' . $params_query); 433 | $request = self::doRequest($url, false, 'DELETE'); 434 | $this->_debug('APIClient DELETE: Sent DELETE REQUEST to ' . $url); 435 | 436 | if ($request['errmsg']) { 437 | $this->_debug('APIClient GET: Error ' . $request['errno'] . ' => ' . $request['errmsg']); 438 | } 439 | 440 | $response = $request['content']; 441 | $response = @json_decode($response); 442 | 443 | $this->_debug("APIClient DELETE: CURL Request \r\n" . var_export($request, true)); 444 | 445 | if (empty($response)) { 446 | return false; 447 | } 448 | 449 | $this->_debug("APIClient DELETE: Response \r\n" . var_export($response, true)); 450 | 451 | return $response; 452 | } 453 | 454 | /** 455 | * Search object in array. 456 | * 457 | * @param $array 458 | * @param $key 459 | * @param $value 460 | * 461 | * @return mixed 462 | */ 463 | public function searchElement($key, $value, $array) 464 | { 465 | if (is_null($value)) { 466 | return null; 467 | } 468 | foreach ($array as $elem) { 469 | if (is_object($elem)) { 470 | if (!empty($elem->{$key}) && $value == $elem->{$key}) { 471 | return $elem; 472 | } 473 | } else { 474 | if (!empty($elem[$key]) && $value == $elem[$key]) { 475 | return $elem; 476 | } 477 | } 478 | } 479 | $this->_debug('APIClient searchElement: Element not found! [' . $key . ' = ' . $value . ']'); 480 | 481 | return null; 482 | } 483 | 484 | /** 485 | * Filter object in array. 486 | * 487 | * @param $key 488 | * @param $value 489 | * @param $array 490 | * @param $limit 491 | * 492 | * @return mixed 493 | */ 494 | public function filterBy($key, $value, $array, $limit = null) 495 | { 496 | if (is_null($value)) { 497 | return null; 498 | } 499 | $result = []; 500 | foreach ($array as $elem) { 501 | if (!empty($limit) && count($result) == $limit) { 502 | break; 503 | } 504 | if (is_object($elem)) { 505 | if (!empty($elem->$key) && $value == $elem->$key) { 506 | $result[] = $elem; 507 | } 508 | } elseif (is_array($elem)) { 509 | if (!empty($elem[$key]) && $value == $elem[$key]) { 510 | $result[] = $elem; 511 | } 512 | } 513 | } 514 | $this->_debug('APIClient filter: Trovati ' . count($result) . ' elementi! [' . $key . ' = ' . $value . ']'); 515 | 516 | return $result; 517 | } 518 | 519 | /** 520 | * Filter object in array. 521 | * 522 | * @param $values 523 | * @param $array 524 | * @param $limit 525 | * 526 | * @return mixed 527 | */ 528 | public function filter($values, $array, $limit = null) 529 | { 530 | if (is_null($values)) { 531 | return null; 532 | } 533 | $result = []; 534 | foreach ($array as $elem) { 535 | if (!empty($limit) && count($result) == $limit) { 536 | break; 537 | } 538 | $found = true; 539 | foreach ($values as $key => $value) { 540 | if (is_object($elem)) { 541 | if (!empty($elem->$key) && $value != $elem->$key) { 542 | $found = false; 543 | } 544 | } elseif (is_array($elem)) { 545 | if (!empty($elem[$key]) && $value != $elem[$key]) { 546 | $found = false; 547 | } 548 | } 549 | } 550 | if ($found) { 551 | $result[] = $elem; 552 | } 553 | } 554 | $this->_debug('APIClient filter: Trovati ' . count($result) . ' elementi! [' . $key . ' = ' . $value . ']'); 555 | 556 | return $result; 557 | } 558 | 559 | private function __clone() 560 | { 561 | } 562 | 563 | private function __wakeup() 564 | { 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marcocesarato/database-api", 3 | "description": "Dynamically generate RESTful APIs from the contents of a database table. Provides JSON, XML, and HTML. Supports most popular databases", 4 | "license": "GPL-3.0-or-later", 5 | "type": "project", 6 | "minimum-stability": "stable", 7 | "authors": [ 8 | { 9 | "name": "Marco Cesarato", 10 | "email": "cesarato.developer@gmail.com" 11 | } 12 | ], 13 | "scripts": { 14 | "post-install-cmd": "vendor/bin/cghooks add --ignore-lock", 15 | "post-update-cmd": "vendor/bin/cghooks update", 16 | "fix-cs": "vendor/bin/php-cs-fixer fix --config=.php_cs.php -v", 17 | "check-cs": "vendor/bin/php-cs-fixer fix --dry-run --format=txt --verbose --diff --diff-format=udiff --config=.php_cs.php" 18 | }, 19 | "require": { 20 | "php": ">=5.6", 21 | "ext-simplexml": "*", 22 | "ext-dom": "*", 23 | "ext-json": "*", 24 | "ext-pdo": "*", 25 | "ext-iconv": "*", 26 | "ext-curl": "*" 27 | }, 28 | "require-dev": { 29 | "brainmaestro/composer-git-hooks": "*", 30 | "friendsofphp/php-cs-fixer": "*" 31 | }, 32 | "config": { 33 | "process-timeout": 0, 34 | "sort-packages": true 35 | }, 36 | "extra": { 37 | "hooks": { 38 | "pre-commit": [ 39 | "vendor/bin/php-cs-fixer fix --config=.cs.php" 40 | ], 41 | "pre-push": [ 42 | "vendor/bin/php-cs-fixer fix --dry-run --format=txt --verbose --diff --diff-format=udiff --config=.cs.php" 43 | ], 44 | "post-merge": "composer install" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 7 | * 8 | * @see https://github.com/marcocesarato/Database-Web-API 9 | */ 10 | require_once __API_ROOT__ . '/docs.php'; 11 | 12 | define('__API_NAME__', 'Database Web API'); // API Name 13 | 14 | $users_table = 'users'; // Table where users are stored 15 | 16 | // REMOVE COMMENT FOR ENABLE TOKEN AUTHENTICATION 17 | /*define("__API_AUTH__", serialize(array( // Set null for disable authentication 18 | 'sqlite' => false, // Enabled save token on SQLite file 19 | 'sqlite_database' => 'api_token', // SQLite filename (only with sqlite = true) 20 | 'api_database' => 'dataset', // Authentication database 21 | 'api_table' => 'api_authentications', // API token table name 22 | 'users' => array( 23 | 'database' => 'dataset', // Database where users are stored 24 | 'table' => $users_table, // Table where users are stored 25 | 'columns' => array( 26 | 'id' => 'user_id', // Id column name 27 | 'username' => 'user_name', // Username column name 28 | 'password' => 'password', // Password column name 29 | 'admin' => array('is_admin' => 1) // Admin bypass condition. With this condition true API bypass all black/whitelists and permissions. Set NULL for disable 30 | ), 31 | 'search' => array('user_id', 'email', 'username'), // Search user by these fields 32 | 'check' => array('active' => 1) // Some validation checks. In this case if the column 'active' with value '1'. Set NULL for disable 33 | ), 34 | )));*/ 35 | 36 | // Datasets (list of database to connect) 37 | define('__API_DATASETS__', serialize([ 38 | 'dataset' => [ 39 | 'name' => 'database_name', 40 | 'username' => 'root', // root is default 41 | 'password' => 'root', // root is default 42 | 'server' => 'localhost', // localhost default 43 | 'port' => 3306, // 3306 is default 44 | 'ttl' => 1, // Cache time to live. Disable cache (1 second only) 45 | 'type' => 'mysql', // mysql is default 46 | 'table_docs' => $docs['dataset'], 47 | 'table_list' => [], // Tables's whitelist (Allow only the tables in this list, if empty allow all) 48 | 'table_blacklist' => [/*blacklist users table*/ 49 | $users_table, 50 | ], // Tables's blacklist 51 | 'column_list' => [], // Columns's whitelist (Allow only the columns in this list, if empty allow all) 52 | 'column_blacklist' => [], // Columns's blacklist 53 | ], 54 | ])); 55 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/cover.png -------------------------------------------------------------------------------- /docs.php: -------------------------------------------------------------------------------- 1 | [ 5 | /** 6 | * @example 7 | * "example" => [ 8 | * "id" => [ 9 | * "description" => "Row identification", 10 | * "example" => "1", 11 | * ], 12 | * "guid" => [ 13 | * "description" => "Standard row identification GUID", 14 | * "example" => "cc7c0e6f-2662-8451-1e70-5c61496c3f5f", 15 | * ], 16 | * ], 17 | */ 18 | ], 19 | ]; 20 | -------------------------------------------------------------------------------- /includes/classes/Auth.php: -------------------------------------------------------------------------------- 1 | 12 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 13 | * 14 | * @see https://github.com/marcocesarato/Database-Web-API 15 | */ 16 | class Auth 17 | { 18 | /** 19 | * Tokens storage table. 20 | * 21 | * @var string 22 | */ 23 | public static $api_table = 'api_auth'; 24 | 25 | /** 26 | * @var self 27 | */ 28 | public static $instance; 29 | /** 30 | * @var API 31 | */ 32 | public $api; 33 | /** 34 | * @var Hooks 35 | */ 36 | public $hooks; 37 | /** 38 | * @var Logger 39 | */ 40 | public $logger; 41 | /** 42 | * @var Request 43 | */ 44 | public static $settings; 45 | /** 46 | * User data. 47 | * 48 | * @var object 49 | */ 50 | public $user; 51 | /** 52 | * Account associated to the user. 53 | * 54 | * @var object 55 | */ 56 | public $account; 57 | public $user_id; 58 | public $is_admin = false; 59 | public $authenticated = false; 60 | /** 61 | * @var PDO 62 | */ 63 | private $db; 64 | private $table_free = []; 65 | private $table_readonly = []; 66 | private $query = []; 67 | 68 | /** 69 | * Singleton constructor. 70 | */ 71 | public function __construct() 72 | { 73 | self::$instance = &$this; 74 | $this->logger = Logger::getInstance(); 75 | $this->hooks = Hooks::getInstance(); 76 | } 77 | 78 | /** 79 | * Get user row. 80 | * 81 | * @return object 82 | */ 83 | public static function getUser() 84 | { 85 | return self::getInstance()->user; 86 | } 87 | 88 | /** 89 | * Returns static reference to the class instance. 90 | */ 91 | public static function &getInstance() 92 | { 93 | return self::$instance; 94 | } 95 | 96 | /** 97 | * Returns if authentication is valid. 98 | * 99 | * @param $query 100 | * 101 | * @return bool 102 | */ 103 | public function validate($query) 104 | { 105 | $this->api = API::getInstance(); 106 | 107 | if (!empty($this->query['db'])) { 108 | $this->api->setDatabase(); 109 | $db_settings = $this->api->getDatabase(); 110 | $this->table_free = $db_settings->table_free; 111 | $this->table_readonly = $db_settings->table_readonly; 112 | } 113 | 114 | $this->query = $query; 115 | 116 | if (defined('__API_AUTH__')) { 117 | self::$settings = unserialize(__API_AUTH__); 118 | if (!empty(self::$settings['api_table'])) { 119 | self::$api_table = preg_replace('/\s+/', '', self::$settings['api_table']); 120 | } 121 | } else { 122 | return true; 123 | } 124 | 125 | if (!$this->api->tableExists(self::$api_table, self::$settings['api_database'])) { 126 | $this->createAPITable(); //create the table 127 | } else { 128 | $this->checkAPITable(); 129 | } 130 | 131 | if (!empty($this->query['check_counter']) && $this->validateToken($this->query['token']) && $this->isAdmin()) { 132 | $this->checkCounter(); 133 | } elseif (!empty($this->query['check_token']) && $this->validateToken($this->query['check_token'])) { 134 | $this->checkToken(); 135 | } elseif (!empty($this->query['token']) && $this->validateToken($this->query['token'])) { 136 | return true; 137 | } elseif (($login_action = $this->hooks->apply_filters('auth_login_request', false, $this->query)) && $this->hooks->has_action($login_action)) { 138 | // Login custom 139 | $this->hooks->do_action($login_action); 140 | } elseif (!empty($this->query['user_id']) && !empty($this->query['password'])) { 141 | $bind_values = []; 142 | 143 | $users_table = self::$settings['users']['table']; 144 | $users_columns = self::$settings['users']['columns']; 145 | 146 | $user = strtolower($query['user_id']); 147 | 148 | $where = []; 149 | foreach (self::$settings['users']['search'] as $col) { 150 | $bind_values[$col] = $user; 151 | $where[$col] = "$col = :$col"; 152 | } 153 | $where_sql = implode(' OR ', $where); 154 | 155 | if (!empty(self::$settings['users']['check'])) { 156 | $where = []; 157 | foreach (self::$settings['users']['check'] as $col => $value) { 158 | $bind_values[$col] = $value; 159 | $where[$col] = "$col = :$col"; 160 | } 161 | $where_sql = (!empty($where_sql) ? " ($where_sql) AND " : '') . implode(' OR ', $where); 162 | } 163 | 164 | $this->api = API::getInstance(); 165 | $this->users_db = $this->getUsersDatabase(); 166 | 167 | $sth = $this->users_db->prepare("SELECT * FROM $users_table WHERE $where_sql"); 168 | foreach ($bind_values as $col => $value) { 169 | $sth->bindParam(":$col", $value); 170 | } 171 | 172 | $sth->execute(); 173 | 174 | $user_row = $sth->fetch(); 175 | 176 | $is_valid = $this->hooks->apply_filters('auth_validate_token', !empty($user_row), $user_row); 177 | 178 | if ($is_valid) { 179 | $password = $query['password']; 180 | if ($user_row[$users_columns['password']] == $password) { 181 | $token = $this->generateToken($user_row[$users_columns['id']], $user_row[$users_columns['username']]); 182 | $this->user_id = $user_row[$users_columns['id']]; 183 | $this->setIsAdmin(!empty($users_columns['admin']) ? $user_row[key(reset($users_columns['admin']))] : false); 184 | // Render 185 | $results = [ 186 | (object)[ 187 | 'token' => $token, 188 | ], 189 | ]; 190 | $results = $this->hooks->apply_filters('auth_login', $results); 191 | exit($this->api->render($results)); 192 | } 193 | } 194 | Response::error('Invalid authentication!', 401); 195 | } 196 | Response::error('Forbidden!', 403); 197 | 198 | return false; 199 | } 200 | 201 | /** 202 | * Get API Database. 203 | * 204 | * @return PDO 205 | */ 206 | public function getAPIDatabase() 207 | { 208 | if (self::$settings['sqlite']) { 209 | return new PDO('sqlite:' . self::$settings['sqlite_filename'] . '.sqlite'); 210 | } 211 | 212 | return $this->api->connect(self::$settings['api_database']); 213 | } 214 | 215 | /** 216 | * Get Users database. 217 | * 218 | * @return mixed 219 | */ 220 | public function getUsersDatabase() 221 | { 222 | return $this->api->connect(self::$settings['users']['database']); 223 | } 224 | 225 | /** 226 | * Create database table. 227 | */ 228 | private function createAPITable() 229 | { 230 | try { 231 | $this->db->exec(' 232 | CREATE TABLE ' . self::$api_table . ' ( 233 | token CHAR(32) PRIMARY KEY, 234 | user_id VARCHAR(255) NOT NULL, 235 | user_name VARCHAR(255) NOT NULL, 236 | user_agent VARCHAR(255) DEFAULT NULL, 237 | counter INT DEFAULT 0 NOT NULL, 238 | date_created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 239 | last_access TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 240 | )'); 241 | } catch (PDOException $e) { 242 | Response::error($e->getMessage(), 500); 243 | } 244 | } 245 | 246 | /** 247 | * Check database table. 248 | */ 249 | private function checkAPITable() 250 | { 251 | $this->db = $this->getAPIDatabase(); 252 | try { 253 | $date = date('Y-m-d H:i:s', strtotime('-1 month')); 254 | $this->db->exec('DELETE FROM ' . self::$api_table . " WHERE last_access != date_created AND last_access < '" . $date . "'"); 255 | } catch (PDOException $e) { 256 | Response::error($e->getMessage(), 500); 257 | } 258 | } 259 | 260 | /** 261 | * Validate Token. 262 | * 263 | * @param $token 264 | * 265 | * @return bool 266 | */ 267 | private function validateToken($token) 268 | { 269 | if (empty(self::$settings)) { 270 | return true; 271 | } 272 | 273 | $users_table = self::$settings['users']['table']; 274 | $users_columns = self::$settings['users']['columns']; 275 | 276 | $this->db = $this->getAPIDatabase(); 277 | 278 | try { 279 | $sth = $this->db->prepare('SELECT * FROM ' . self::$api_table . ' WHERE token = :token'); 280 | $sth->bindParam(':token', $token); 281 | $sth->execute(); 282 | $token_row = $sth->fetch(); 283 | 284 | $exists = $this->hooks->apply_filters('auth_validate_token', !empty($token_row), $token); 285 | 286 | $auth_bypass = false; 287 | $auth_bypass = $this->hooks->apply_filters('auth_bypass', $auth_bypass); 288 | 289 | // Bypass 290 | if (!$exists && $auth_bypass && empty($this->query['force_validation'])) { 291 | $exists = true; 292 | $token_row = []; 293 | $token_row['user_id'] = '1'; 294 | $token_row['counter'] = 0; 295 | } 296 | 297 | if ($exists) { 298 | $this->api = API::getInstance(); 299 | $this->users_db = $this->getUsersDatabase(); 300 | $sth = $this->users_db->prepare("SELECT * FROM $users_table WHERE " . $users_columns['id'] . ' = :user_id'); 301 | $sth->bindParam(':user_id', $token_row['user_id']); 302 | 303 | $sth->execute(); 304 | $user_row = $sth->fetch(); 305 | 306 | if (!empty($user_row)) { 307 | $this->db = $this->getAPIDatabase(); 308 | $sth = $this->db->prepare('UPDATE ' . self::$api_table . ' SET last_access = :last_access, counter = :counter WHERE token = :token'); 309 | $last_access = date('Y-m-d H:i:s'); 310 | $counter = $this->needIncrementCounter() ? intval($token_row['counter']) + 1 : intval($token_row['counter']); 311 | $sth->bindParam(':counter', $counter); 312 | $sth->bindParam(':last_access', $last_access); 313 | $sth->bindParam(':token', $token); 314 | $sth->execute(); 315 | 316 | $this->user = $user_row; 317 | $this->user_id = $user_row['id']; 318 | if (!empty($users_columns['admin'])) { 319 | $this->setIsAdmin(($user_row[key($users_columns['admin'])] == reset($users_columns['admin'])) ? true : false); 320 | } 321 | $this->authenticated = true; 322 | 323 | return true; 324 | } 325 | } 326 | 327 | return false; 328 | } catch (PDOException $e) { 329 | Response::error($e->getMessage(), 500); 330 | 331 | return false; 332 | } 333 | } 334 | 335 | /** 336 | * Check counter. 337 | */ 338 | private function checkCounter() 339 | { 340 | $this->db = $this->getAPIDatabase(); 341 | try { 342 | $sth = $this->db->prepare('SELECT user_id, user_name, SUM(counter) as counter FROM ' . self::$api_table . ' GROUP BY user_id, user_name'); 343 | $sth->execute(); 344 | $results = $sth->fetchAll(PDO::FETCH_OBJ); 345 | $this->api->render($results); 346 | } catch (PDOException $e) { 347 | Response::error($e->getMessage(), 500); 348 | } 349 | } 350 | 351 | /** 352 | * Check token. 353 | */ 354 | private function checkToken() 355 | { 356 | try { 357 | $results = [ 358 | 'user' => (object)[ 359 | 'id' => $this->user_id, 360 | 'is_admin' => $this->is_admin, 361 | ], 362 | 'response' => (object)['status' => 200, 'message' => 'OK'], 363 | ]; 364 | 365 | $this->logger->debug($results); 366 | $results = $this->hooks->apply_filters('auth_token_check', $results); 367 | $this->api->render($results); 368 | } catch (PDOException $e) { 369 | Response::error($e->getMessage(), 500); 370 | } 371 | } 372 | 373 | /** 374 | * Generate Token. 375 | * 376 | * @param $user_id 377 | * @param $user_name 378 | * 379 | * @return string|null 380 | */ 381 | public function generateToken($user_id, $user_name) 382 | { 383 | $this->db = $this->getAPIDatabase(); 384 | try { 385 | $token = md5(uniqid(mt_rand(), true)); 386 | $sth = $this->db->prepare('INSERT INTO ' . self::$api_table . ' (token,user_id,user_name,user_agent) VALUES (:token,:user_id,:user_name,:user_agent)'); 387 | $sth->bindParam(':token', $token); 388 | $sth->bindParam(':user_name', $user_name); 389 | $sth->bindParam(':user_id', $user_id); 390 | $sth->bindParam(':user_agent', $_SERVER['HTTP_USER_AGENT']); 391 | if ($sth->execute()) { 392 | return $token; 393 | } 394 | 395 | return null; 396 | } catch (PDOException $e) { 397 | Response::error($e->getMessage(), 500); 398 | } 399 | 400 | return null; 401 | } 402 | 403 | /** 404 | * Generate restricted condition for sql queries. 405 | * 406 | * @param $table 407 | * @param $permission 408 | * 409 | * @return string 410 | */ 411 | public function permissionSQL($table, $permission) 412 | { 413 | $sql = ''; 414 | 415 | // All allowed 416 | if ($this->isAdmin()) { 417 | $sql = "'1' = '1'"; 418 | } 419 | 420 | $sql = $this->hooks->apply_filters('sql_restriction', $sql, $table, $permission); 421 | 422 | return $sql; 423 | } 424 | 425 | // Tables with no permission required (readonly) 426 | 427 | /** 428 | * Can Read. 429 | * 430 | * @param $table 431 | * 432 | * @return bool 433 | */ 434 | public function canRead($table) 435 | { 436 | $result = true; 437 | 438 | if (empty(self::$settings)) { 439 | $result = true; 440 | } else { 441 | if (in_array($table, $this->table_free)) { 442 | $result = true; 443 | } 444 | } 445 | 446 | $result = $this->hooks->apply_filters('can_read', $result, $table); 447 | 448 | return $result; 449 | } 450 | 451 | /** 452 | * Can Write. 453 | * 454 | * @param $table 455 | * 456 | * @return bool 457 | */ 458 | public function canWrite($table) 459 | { 460 | $result = false; 461 | 462 | if (in_array($table, $this->table_readonly)) { 463 | $result = false; 464 | } else { 465 | if (empty(self::$settings)) { 466 | $result = false; 467 | } else { 468 | if (in_array($table, $this->table_free)) { 469 | $result = true; 470 | } 471 | } 472 | } 473 | 474 | $result = $this->hooks->apply_filters('can_write', $result, $table); 475 | 476 | return $result; 477 | } 478 | 479 | /** 480 | * Can edit. 481 | * 482 | * @param $table 483 | * 484 | * @return bool 485 | */ 486 | public function canEdit($table) 487 | { 488 | $result = false; 489 | 490 | if (in_array($table, $this->table_readonly)) { 491 | $result = false; 492 | } else { 493 | if (empty(self::$settings)) { 494 | $result = false; 495 | } else { 496 | if (in_array($table, $this->table_free)) { 497 | $result = true; 498 | } 499 | } 500 | } 501 | 502 | $result = $this->hooks->apply_filters('can_edit', $result, $table); 503 | 504 | return $result; 505 | } 506 | 507 | /** 508 | * Can delete. 509 | * 510 | * @param $table 511 | * 512 | * @return bool 513 | */ 514 | public function canDelete($table) 515 | { 516 | $result = false; 517 | 518 | if (in_array($table, $this->table_readonly)) { 519 | $result = false; 520 | } else { 521 | if (empty(self::$settings)) { 522 | $result = false; 523 | } else { 524 | if (in_array($table, $this->table_free)) { 525 | $result = true; 526 | } 527 | } 528 | } 529 | 530 | $result = $this->hooks->apply_filters('can_delete', $result, $table); 531 | 532 | return $result; 533 | } 534 | 535 | /** 536 | * Increment counter. 537 | * 538 | * @return bool 539 | */ 540 | private function needIncrementCounter() 541 | { 542 | return !( 543 | !empty($this->query['docs']) || 544 | !empty($this->query['check_token']) || 545 | !empty($this->query['check_counter']) || 546 | ( 547 | !empty($this->query['user_id']) && 548 | !empty($this->query['password']) 549 | ) 550 | ); 551 | } 552 | 553 | /** 554 | * @return bool 555 | */ 556 | public function isAuthenticated() 557 | { 558 | return !empty(self::getUser()); 559 | } 560 | 561 | /** 562 | * @return bool 563 | */ 564 | public function isAdmin() 565 | { 566 | return $this->is_admin === true; 567 | } 568 | 569 | /** 570 | * @param bool $is_admin 571 | * 572 | * @return self 573 | */ 574 | public function setIsAdmin($is_admin) 575 | { 576 | $this->is_admin = $is_admin; 577 | 578 | return $this; 579 | } 580 | } 581 | 582 | $AUTH = new Auth(); 583 | -------------------------------------------------------------------------------- /includes/classes/DatabaseErrors.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class DatabaseErrors 14 | { 15 | public function __construct() 16 | { 17 | } 18 | 19 | public static function errorMessage($error) 20 | { 21 | $code = $error->getCode(); 22 | $message = $error->getMessage(); 23 | 24 | $error = "($code) Oops something's gone wrong!"; 25 | 26 | $logger = Logger::getInstance(); 27 | $logger->error("($code) $message"); 28 | 29 | return $error; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /includes/classes/Docs.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class Docs 14 | { 15 | public static $instance; 16 | 17 | private $api; 18 | private $db; 19 | private $logger; 20 | private $hooks; 21 | 22 | /** 23 | * Singleton constructor. 24 | */ 25 | public function __construct() 26 | { 27 | self::$instance = &$this; 28 | $this->logger = Logger::getInstance(); 29 | $this->hooks = Hooks::getInstance(); 30 | $this->api = API::getInstance(); 31 | $this->db = &$this->api->connect(); 32 | } 33 | 34 | /** 35 | * Returns static reference to the class instance. 36 | */ 37 | public static function &getInstance() 38 | { 39 | return self::$instance; 40 | } 41 | 42 | /** 43 | * Generate Open API documentation. 44 | * 45 | * @return array 46 | */ 47 | public function generate() 48 | { 49 | $docs = [ 50 | 'openapi' => '3.0.0', 51 | 'servers' => [ 52 | [ 53 | 'url' => base_domain(), 54 | ], 55 | ], 56 | 'info' => [ 57 | 'description' => 'Neterprise API documentation', 58 | 'version' => '3.0.0', 59 | 'title' => 'Neterprise API', 60 | ], 61 | 'paths' => [], 62 | 'components' => [ 63 | 'securitySchemes' => [ 64 | 'api_key' => [ 65 | 'type' => 'apiKey', 66 | 'name' => 'Access-Token', 67 | 'in' => 'header', 68 | ], 69 | ], 70 | ], 71 | ]; 72 | 73 | $methods = ['POST', 'GET', 'PATCH', 'PUT', 'DELETE']; 74 | $contentTypes = ['application/json']; 75 | $tables = $this->api->getDatabase()->table_list; 76 | $schemas = []; 77 | if (empty($tables)) { 78 | $tables = $this->api->getTables(); 79 | } 80 | foreach ($tables as $table) { 81 | if ($this->api->checkTable($table)) { 82 | $path = build_base_url('/' . $table . '.' . $this->api->query['format']); 83 | $parse = parse_url($path); 84 | $path = $parse['path']; 85 | $tag = ucwords(str_replace('_', ' ', $table)); 86 | foreach ($methods as $method) { 87 | $methodLower = strtolower($method); 88 | $tableId = str_replace(' ', '', $tag); 89 | $operationId = $methodLower . $tableId; 90 | 91 | $results = $this->api->getTableMeta($table, $this->api->db); 92 | $proprieties = []; 93 | 94 | $required = []; 95 | foreach ($results as $column) { 96 | if ($this->api->checkColumn($column['column_name'], $table)) { 97 | $docs_table = !empty($this->api->db->table_docs[$table]) ? $this->api->db->table_docs[$table] : null; 98 | $docs_column = !empty($docs_table[$column['column_name']]) ? $docs_table[$column['column_name']] : null; 99 | 100 | if (!empty($docs_table) && empty($docs_column)) { 101 | continue; 102 | } 103 | 104 | $tmp = [ 105 | 'type' => $this->convertDataType($column['data_type']), 106 | 'description' => '', 107 | 'example' => '', 108 | ]; 109 | 110 | if (strtolower($column['is_nullable']) === 'no') { 111 | $required[] = $column['column_name']; 112 | } 113 | 114 | if (!empty($column['character_maximum_length'])) { 115 | $tmp['maxLength'] = $column['character_maximum_length']; 116 | } 117 | if (!empty($docs_column) && is_array($docs_column)) { 118 | if (!empty($docs_column['description'])) { 119 | $tmp['description'] = ucfirst($docs_column['description']); 120 | } 121 | if (!empty($docs_column['example'])) { 122 | $cleaned = trim($docs_column['example'], "'"); 123 | $tmp['example'] = !empty($cleaned) ? $cleaned : $docs_column['example']; 124 | } 125 | } 126 | $proprieties[$column['column_name']] = $tmp; 127 | } 128 | } 129 | 130 | $status = ($method === 'POST') ? 201 : 200; 131 | 132 | $schemas[$tableId] = [ 133 | 'properties' => $proprieties, 134 | 'type' => 'object', 135 | 'required' => $required, 136 | ]; 137 | 138 | $content = []; 139 | foreach ($contentTypes as $contentType) { 140 | $content[$contentType] = [ 141 | 'schema' => [ 142 | '$ref' => "#/components/schemas/$tableId", 143 | ], 144 | ]; 145 | } 146 | 147 | $docs['paths'][$path][$methodLower] = [ 148 | 'summary' => "$tag resource $method operation", 149 | 'operationId' => $operationId, 150 | 'tags' => [$tag], 151 | 'responses' => [ 152 | $status => [ 153 | 'description' => 'OK', 154 | ], 155 | '400' => [ 156 | 'description' => 'Bad Request', 157 | ], 158 | ], 159 | ]; 160 | 161 | if (in_array($method, ['POST', 'PUT', 'PATCH'])) { 162 | $docs['paths'][$path][$methodLower]['requestBody'] = [ 163 | 'required' => true, 164 | 'content' => $content, 165 | ]; 166 | } 167 | } 168 | } 169 | } 170 | 171 | $docs['components']['schemas'] = $schemas; 172 | 173 | return $docs; 174 | } 175 | 176 | /** 177 | * Database type to Open API. 178 | * 179 | * @param $type 180 | * 181 | * @return string 182 | */ 183 | private function convertDataType($type) 184 | { 185 | $type = strtolower($type); 186 | switch ($type) { 187 | case 'bigint': 188 | case 'int8': 189 | case 'serial8': 190 | case 'bigserial': 191 | return 'integer'; //long 192 | 193 | case 'bit': 194 | case 'int': 195 | case 'smallint': 196 | case 'int2': 197 | case 'integer': 198 | case 'int4': 199 | case 'smallserial': 200 | case 'serial2': 201 | case 'serial': 202 | case 'serial4': 203 | return 'integer'; 204 | 205 | case 'real': 206 | case 'float4': 207 | case 'double precision': 208 | case 'float8': 209 | case 'decimal': 210 | case 'double': 211 | return 'number'; // float 212 | 213 | case 'boolean': 214 | case 'bool': 215 | return 'boolean'; 216 | 217 | case 'bpchar': 218 | case 'varchar': 219 | case 'char': 220 | case 'character varying': 221 | case 'character': 222 | case 'bit varying': 223 | case 'text': 224 | return 'string'; 225 | 226 | case 'date': 227 | return 'string'; // date 228 | 229 | case 'time': 230 | case 'timestamp': 231 | return 'string'; // dateTime 232 | } 233 | 234 | return $type; 235 | } 236 | } 237 | 238 | $DOCS = new Docs(); 239 | -------------------------------------------------------------------------------- /includes/classes/Dump.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class Dump 14 | { 15 | private static $highlight = true; 16 | private static $depth = 10; 17 | 18 | private static $_objects; 19 | private static $_output; 20 | 21 | /** 22 | * Die with dump. 23 | */ 24 | public static function out() 25 | { 26 | self::setHeader(); 27 | $args = func_get_args(); 28 | echo self::internalDump($args); 29 | } 30 | 31 | /** 32 | * Die with dump. 33 | */ 34 | public static function fatal() 35 | { 36 | self::setHeader(); 37 | $args = func_get_args(); 38 | exit(self::internalDump($args)); 39 | } 40 | 41 | /** 42 | * Clean die with dump. 43 | */ 44 | public static function clean() 45 | { 46 | self::setHeader(); 47 | ob_clean(); 48 | $args = func_get_args(); 49 | exit(self::internalDump($args)); 50 | } 51 | 52 | /** 53 | * Get dump. 54 | * 55 | * @return string 56 | */ 57 | public static function get() 58 | { 59 | $args = func_get_args(); 60 | 61 | return self::internalDump($args); 62 | } 63 | 64 | /** 65 | * Get Highlight. 66 | * 67 | * @return bool 68 | */ 69 | public static function getHighlight() 70 | { 71 | return self::$highlight; 72 | } 73 | 74 | /** 75 | * Enable Highlight. 76 | */ 77 | public static function enableHighlight() 78 | { 79 | self::$highlight = true; 80 | } 81 | 82 | /** 83 | * Disable Highlight. 84 | */ 85 | public static function disableHighlight() 86 | { 87 | self::$highlight = false; 88 | } 89 | 90 | /** 91 | * Get Depth. 92 | * 93 | * @return int 94 | */ 95 | public static function getDepth() 96 | { 97 | return self::$depth; 98 | } 99 | 100 | /** 101 | * Set Depth. 102 | * 103 | * @param int $depth 104 | */ 105 | public static function setDepth($depth) 106 | { 107 | self::$depth = $depth; 108 | } 109 | 110 | /** 111 | * Set Header. 112 | */ 113 | private static function setHeader() 114 | { 115 | if (Request::isConsole()) { 116 | self::disableHighlight(); 117 | } 118 | if (self::$highlight) { 119 | header('Content-Type: text/html'); 120 | } else { 121 | header('Content-Type: text/plain'); 122 | } 123 | } 124 | 125 | /** 126 | * Internal Dump. 127 | * 128 | * @param array $func_args 129 | * 130 | * @return string 131 | */ 132 | private static function internalDump($func_args) 133 | { 134 | $dump = ''; 135 | $args = self::parseArgs($func_args); 136 | if (count($func_args) > 1) { 137 | foreach ($func_args as $var) { 138 | $dump .= self::generateDump($var); 139 | } 140 | } else { 141 | $dump = self::generateDump($args); 142 | } 143 | 144 | return $dump; 145 | } 146 | 147 | /** 148 | * Converts a variable into a string representation. 149 | * This method achieves the similar functionality as var_dump and print_r 150 | * but is more robust when handling complex objects such as PRADO controls. 151 | * 152 | * @param mixed variable to be dumped 153 | * @param int maximum depth that the dumper should go into the variable. Defaults to 10. 154 | * 155 | * @return string the string representation of the variable 156 | */ 157 | private static function generateDump($var) 158 | { 159 | self::$_output = ''; 160 | self::$_objects = []; 161 | self::parseDump($var, 0); 162 | if (self::$highlight) { 163 | $result = highlight_string("/', '', $result, 1); 166 | } else { 167 | $result = self::$_output; 168 | } 169 | 170 | return $result; 171 | } 172 | 173 | /** 174 | * Parse args. 175 | * 176 | * @param $args 177 | * 178 | * @return mixed 179 | */ 180 | private static function parseArgs($args) 181 | { 182 | if (count($args) < 2) { 183 | $args = $args[0]; 184 | } 185 | 186 | return $args; 187 | } 188 | 189 | /** 190 | * Parse Dump. 191 | * 192 | * @param $var 193 | * @param $level 194 | * 195 | * @return string 196 | */ 197 | private static function parseDump($var, $level) 198 | { 199 | switch (gettype($var)) { 200 | case 'boolean': 201 | self::$_output .= $var ? 'true' : 'false'; 202 | break; 203 | case 'integer': 204 | self::$_output .= "$var"; 205 | break; 206 | case 'double': 207 | self::$_output .= "$var"; 208 | break; 209 | case 'string': 210 | self::$_output .= "'$var'"; 211 | break; 212 | case 'resource': 213 | self::$_output .= '{resource}'; 214 | break; 215 | case 'NULL': 216 | self::$_output .= 'null'; 217 | break; 218 | case 'unknown type': 219 | self::$_output .= '{unknown}'; 220 | break; 221 | case 'array': 222 | if (self::$depth <= $level) { 223 | self::$_output .= 'array(...)'; 224 | } elseif (empty($var)) { 225 | self::$_output .= 'array()'; 226 | } else { 227 | $keys = array_keys($var); 228 | $spaces = str_repeat(' ', $level * 4); 229 | self::$_output .= "array\n" . $spaces . '('; 230 | foreach ($keys as $key) { 231 | self::$_output .= "\n" . $spaces . " [$key] => "; 232 | self::$_output .= self::parseDump($var[$key], $level + 1); 233 | } 234 | self::$_output .= "\n" . $spaces . ')'; 235 | } 236 | break; 237 | case 'object': 238 | if (($id = array_search($var, self::$_objects, true)) !== false) { 239 | self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; 240 | } elseif (self::$depth <= $level) { 241 | self::$_output .= get_class($var) . '(...)'; 242 | } else { 243 | $id = array_push(self::$_objects, $var); 244 | $className = get_class($var); 245 | $members = (array)$var; 246 | $keys = array_keys($members); 247 | $spaces = str_repeat(' ', $level * 4); 248 | self::$_output .= "$className#$id\n" . $spaces . '('; 249 | foreach ($keys as $key) { 250 | $keyDisplay = strtr(trim($key), ["\0" => ':']); 251 | self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; 252 | self::$_output .= self::parseDump($members[$key], $level + 1); 253 | } 254 | self::$_output .= "\n" . $spaces . ')'; 255 | } 256 | break; 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /includes/classes/Logger.php: -------------------------------------------------------------------------------- 1 | 11 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 12 | * 13 | * @see https://github.com/marcocesarato/Database-Web-API 14 | */ 15 | class Logger 16 | { 17 | protected static $instances = []; 18 | protected static $session_token; 19 | protected $log_file; 20 | protected $log_dir; 21 | protected $log_path; 22 | protected $file; 23 | protected $params = []; 24 | protected $options = [ 25 | 'dateFormat' => 'd-M-Y H:i:s.u', 26 | 'onlyMessage' => false, 27 | ]; 28 | protected $cache = ''; 29 | protected $previously_enabled_memory_cache = false; 30 | protected $use_memory_cache = false; 31 | 32 | /** 33 | * Get an instance of the logger. 34 | * 35 | * @param $key 36 | * 37 | * @return self 38 | */ 39 | public static function getInstance($key = 'API') 40 | { 41 | if (empty(self::$session_token)) { 42 | self::$session_token = uniqid('session_', false); 43 | } 44 | if (!array_key_exists($key, self::$instances)) { 45 | self::$instances[$key] = new self(); 46 | } 47 | 48 | return self::$instances[$key]; 49 | } 50 | 51 | /** 52 | * Logger constructor. 53 | * Prevent creating multiple instances due to "protected" constructor. 54 | */ 55 | protected function __construct() 56 | { 57 | } 58 | 59 | /** 60 | * Prevent the instance from being cloned. 61 | */ 62 | protected function __clone() 63 | { 64 | } 65 | 66 | /** 67 | * Prevent from being unserialized. 68 | */ 69 | protected function __wakeup() 70 | { 71 | } 72 | 73 | /** 74 | * Setup. 75 | * 76 | * @param string $log_file - path and filename of log 77 | * @param array $params 78 | */ 79 | public function setLog($log_dir, $log_file = 'logs.log', $params = []) 80 | { 81 | $this->log_dir = $log_dir; 82 | $this->log_file = $log_file; 83 | $this->log_path = preg_replace('/\\\\/', '\\', $log_dir . '/' . $log_file); 84 | $this->params = shortcode_atts($this->options, $params); 85 | 86 | // Create log file if it doesn't exist. 87 | if (!file_exists($this->log_path)) { 88 | if (!file_exists(dirname($this->log_path))) { 89 | if (!mkdir($concurrentDirectory = dirname($this->log_path), 0775, true) && !is_dir($concurrentDirectory)) { 90 | //throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); 91 | } 92 | } 93 | @fopen($this->log_path, 'wb'); //or exit("Can't create $log_file!"); 94 | } 95 | // Check permissions of file. 96 | if (!is_writable($this->log_path)) { 97 | //throw exception if not writable 98 | //throw new Exception("ERROR: Unable to write to file!", 1); 99 | } 100 | } 101 | 102 | /** 103 | * Info method (write info message). 104 | * 105 | * @param mixed $message 106 | * 107 | * @return void 108 | */ 109 | public function info($message) 110 | { 111 | $this->writeLog($message, 'INFO'); 112 | } 113 | 114 | /** 115 | * Write to log file. 116 | * 117 | * @param mixed $message 118 | * @param string $severity 119 | * 120 | * @return void 121 | */ 122 | public function writeLog($message, $severity) 123 | { 124 | // Encode to JSON if is not a string 125 | if (!is_string($message)) { 126 | $message = json_encode($message); 127 | } 128 | 129 | // Remove new lines 130 | $message = trim(preg_replace('/\s+/', ' ', $message)); 131 | 132 | $token = Request::getToken(); 133 | 134 | // Request method 135 | $method = Request::method(); 136 | // Grab the url path ( for troubleshooting ) 137 | $path = $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI']; 138 | 139 | // Grab time - based on timezone in php.ini 140 | $t = microtime(true); 141 | $micro = sprintf('%06d', ($t - floor($t)) * 1000000); 142 | $d = new DateTime(date('Y-m-d H:i:s.' . $micro, $t)); 143 | $time = $d->format($this->params['dateFormat']); 144 | 145 | $ip = Request::getIPAddress(); 146 | 147 | $path = str_replace($token . '/', '', $path); 148 | 149 | $session_token = self::$session_token; 150 | 151 | // Write time, url, & message to end of file 152 | if ($this->params['onlyMessage']) { 153 | $log = "[$time] [$severity]: $message"; 154 | } else { 155 | $log = "[$time] [$severity] [method $method] [url $path] [token $token] [client $ip] [session $session_token]: $message"; 156 | } 157 | 158 | $log .= PHP_EOL; 159 | 160 | if ($severity === 'none') { 161 | $log = ''; 162 | } 163 | 164 | if (!$this->use_memory_cache || isset($this->cache[100000])) { // Write every ~ 100kb if cache was enabled 165 | // open log file 166 | if (!is_resource($this->file)) { 167 | $this->openLog(); 168 | } 169 | 170 | @fwrite($this->file, $this->cache . $log); 171 | $this->cache = ''; 172 | } else { 173 | $this->cache .= $log; 174 | } 175 | } 176 | 177 | /** 178 | * Open log file. 179 | * 180 | * @return void 181 | */ 182 | private function openLog() 183 | { 184 | $openFile = $this->log_dir . '/' . $this->log_file; 185 | if (!file_exists(dirname($openFile))) { 186 | if (!mkdir($concurrentDirectory = dirname($openFile), 0775, true) && !is_dir($concurrentDirectory)) { 187 | //throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); 188 | } 189 | } 190 | // 'a' option = place pointer at end of file 191 | $this->file = @fopen($openFile, 'ab'); // or exit("Can't open $openFile!"); 192 | } 193 | 194 | /** 195 | * Debug method (write debug message). 196 | * 197 | * @param mixed $message 198 | * 199 | * @return void 200 | */ 201 | public function debug($message) 202 | { 203 | $this->writeLog($message, 'DEBUG'); 204 | } 205 | 206 | /** 207 | * Warning method (write warning message). 208 | * 209 | * @param mixed $message 210 | * 211 | * @return void 212 | */ 213 | public function warning($message) 214 | { 215 | $this->writeLog($message, 'WARNING'); 216 | } 217 | 218 | /** 219 | * Error method (write error message). 220 | * 221 | * @param mixed $message 222 | * 223 | * @return void 224 | */ 225 | public function error($message) 226 | { 227 | $this->writeLog($message, 'ERROR'); 228 | } 229 | 230 | /** 231 | * Write cache on shutdown (if memory cache enabled and cache is not empty). 232 | */ 233 | public function writeCache() 234 | { 235 | if ($this->use_memory_cache && !empty($this->cache)) { 236 | $this->use_memory_cache = false; 237 | $this->writeLog('', 'none'); 238 | $this->use_memory_cache = true; 239 | } 240 | } 241 | 242 | /** 243 | * Enable/Disable memory cache (for faster execution). 244 | * 245 | * @param bool $status 246 | */ 247 | public function useMemoryCache($status = true) 248 | { 249 | if ($status === false) { 250 | $this->writeCache(); 251 | } 252 | 253 | $this->use_memory_cache = $status; 254 | if ($status === true && !$this->previously_enabled_memory_cache) { 255 | register_shutdown_function([$this, 'writeCache']); 256 | $this->previously_enabled_memory_cache = true; 257 | } 258 | } 259 | 260 | /** 261 | * Class destructor. 262 | */ 263 | public function __destruct() 264 | { 265 | if ($this->file) { 266 | @fclose($this->file); 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /includes/classes/Request.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class Request 14 | { 15 | public static $instance; 16 | public $input; 17 | 18 | private static $urlparsed = false; 19 | 20 | /** 21 | * Request constructor. 22 | */ 23 | public function __construct() 24 | { 25 | self::$instance = &$this; 26 | self::blockBots(); 27 | self::blockTor(); 28 | $this->input = self::getParams(); 29 | } 30 | 31 | /** 32 | * Returns static reference to the class instance. 33 | */ 34 | public static function &getInstance() 35 | { 36 | return self::$instance; 37 | } 38 | 39 | /** 40 | * Returns the request parameters. 41 | * 42 | * @params $sanitize (optional) sanitize input data, default is true 43 | * 44 | * @return $params parameters 45 | */ 46 | public static function getParams($sanitize = true) 47 | { 48 | self::parseUrlRewrite(); 49 | 50 | // Parse GET params 51 | $source = $_SERVER['QUERY_STRING']; 52 | 53 | parse_str($source, $params); 54 | 55 | // Parse POST, PUT, PATCH params 56 | if (!in_array(self::method(), ['GET', 'DELETE'])) { 57 | $source_input = file_get_contents('php://input'); 58 | $decoded_input = json_decode($source_input, true); 59 | if (json_last_error() == JSON_ERROR_NONE && is_array($decoded_input)) { 60 | $params_input = $decoded_input; 61 | } else { 62 | parse_str($source_input, $params_input); 63 | } 64 | $params = array_merge($params, $params_input); 65 | } 66 | 67 | // Read header Access-Token 68 | $params['token'] = self::getToken(); 69 | 70 | // Auth 71 | if (!empty($params['auth'])) { 72 | if (empty($params['user_id'])) { 73 | $params['user_id'] = (!empty($_SERVER['HTTP_AUTH_ACCOUNT']) ? $_SERVER['HTTP_AUTH_ACCOUNT'] : uniqid(mt_rand(), true)); 74 | } 75 | if (empty($params['password'])) { 76 | $params['password'] = (!empty($_SERVER['HTTP_AUTH_PASSWORD']) ? $_SERVER['HTTP_AUTH_PASSWORD'] : uniqid(mt_rand(), true)); 77 | } 78 | unset($params['token']); 79 | } 80 | 81 | // Check token 82 | if (!empty($params['check_auth']) && empty($params['check_token'])) { 83 | $params['check_token'] = (!empty($_SERVER['HTTP_ACCESS_TOKEN']) ? $_SERVER['HTTP_ACCESS_TOKEN'] : uniqid(mt_rand(), true)); 84 | unset($params['token']); 85 | } 86 | 87 | if (empty($params['token'])) { 88 | unset($params['token']); 89 | } 90 | 91 | if ($sanitize == true) { 92 | $params = self::sanitizeParams($params); 93 | } 94 | 95 | return $params; 96 | } 97 | 98 | /** 99 | * Parse url rewrite. 100 | */ 101 | public static function parseUrlRewrite() 102 | { 103 | if (!self::$urlparsed) { 104 | $formats = ['json', 'xml', 'html']; 105 | $rewrite_regex = [ 106 | // Check Auth 107 | 'auth/check' => 'check_auth=1&format=%s', 108 | // Auth 109 | 'auth' => 'auth=1&format=%s', 110 | 111 | /* Token required requests */ 112 | 113 | // Dataset + P1 + P2 + P3 + P4 (Custom requests) 114 | '([^/]+)/([^/]+)/([^/]+)/([^/]+)/([^/]+)' => 'custom=%s&db=%s&table=%s&where[%s]=%s&format=%s', 115 | // Dataset + Table + Column + Value 116 | '([^/]+)/([^/]+)/([^/]+)/([^/]+)' => 'db=%s&table=%s&where[%s]=%s&format=%s', 117 | // Dataset + Limit + Table 118 | '([^/]+)/([0-9]|[1-8][0-9]|9[0-9]|100)/([^/]+)' => 'db=%s&limit=%s&table=%s&format=%s', 119 | // Dataset + Table docs 120 | '([^/]+)/docs/([^/]+)' => 'db=%s&table=%s&format=%s&docs=true', 121 | // Dataset + Table + ID 122 | '([^/]+)/([^/]+)/([^/]+)' => 'db=%s&table=%s&id=%s&format=%s', 123 | // Dataset + Check counter 124 | '([^/]+)/check_counter' => 'db=%s&check_counter=true&format=%s', 125 | // Dataset + Table 126 | '([^/]+)/([^/]+)' => 'db=%s&table=%s&format=%s', 127 | // Dataset (for POST/PUT/PATCH requests) 128 | '([^/]+)' => 'db=%s&format=%s', 129 | ]; 130 | 131 | $formats = implode('|', $formats); 132 | $formats = "\.($formats)$"; 133 | 134 | // Parse rewrites 135 | foreach ($rewrite_regex as $regex => $qs) { 136 | $request_uri = self::getRequestURI(); 137 | if (preg_match('#' . $regex . $formats . '#', $request_uri, $matches)) { 138 | array_shift($matches); 139 | $matches = array_filter($matches, function ($v) { 140 | return urlencode($v); 141 | }); 142 | 143 | $query_string = vsprintf($qs, $matches); 144 | if ($_SERVER['QUERY_STRING'] != $query_string) { 145 | $_SERVER['QUERY_STRING'] = $query_string . (!empty($_SERVER['QUERY_STRING']) ? '&' . $_SERVER['QUERY_STRING'] : ''); 146 | } 147 | parse_str($_SERVER['QUERY_STRING'], $_GET); 148 | break; 149 | } 150 | } 151 | self::$urlparsed = true; 152 | } 153 | } 154 | 155 | /** 156 | * Get Request URI. 157 | * 158 | * @return string 159 | */ 160 | public static function getRequestURI() 161 | { 162 | $base = ''; 163 | $root = dirname($_SERVER['SCRIPT_FILENAME']); 164 | $doc_root = realpath(preg_replace('/' . preg_quote($_SERVER['SCRIPT_NAME'], '/') . '$/', '', $root)); 165 | 166 | if ($root != realpath($_SERVER['DOCUMENT_ROOT'])) { 167 | $base = str_replace(realpath($_SERVER['DOCUMENT_ROOT']), '', $root) . '/'; 168 | } elseif ($root != $doc_root) { 169 | $base = str_replace($doc_root, '', $root) . '/'; 170 | } 171 | $base = str_replace('\\', '/', $base); 172 | 173 | $request_uri = str_replace($base, '', $_SERVER['REQUEST_URI']); 174 | $request_uri = explode('?', $request_uri, 2); 175 | $request_uri = $request_uri[0]; 176 | 177 | $request_uri = str_replace(basename(__API_ROOT__), '/', $request_uri); 178 | 179 | return $request_uri; 180 | } 181 | 182 | /** 183 | * Returns the request method. 184 | */ 185 | public static function method() 186 | { 187 | return $_SERVER['REQUEST_METHOD']; 188 | } 189 | 190 | /** 191 | * Returns the access token. 192 | */ 193 | public static function getToken() 194 | { 195 | $token = @$_GET['token']; 196 | if (isset($_SERVER['HTTP_ACCESS_TOKEN'])) { 197 | $token = $_SERVER['HTTP_ACCESS_TOKEN']; 198 | } 199 | 200 | return $token; 201 | } 202 | 203 | /** 204 | * Prevent bad bots. 205 | */ 206 | public static function blockBots() 207 | { 208 | // Block bots 209 | if (preg_match('/(spider|crawler|slurp|teoma|archive|track|snoopy|lwp|client|libwww)/i', $_SERVER['HTTP_USER_AGENT']) || 210 | preg_match('/(havij|libwww-perl|wget|python|nikto|curl|scan|java|winhttp|clshttp|loader)/i', $_SERVER['HTTP_USER_AGENT']) || 211 | preg_match('/(%0A|%0D|%27|%3C|%3E|%00)/i', $_SERVER['HTTP_USER_AGENT']) || 212 | preg_match("/(;|<|>|'|\"|\)|\(|%0A|%0D|%22|%27|%28|%3C|%3E|%00).*(libwww-perl|wget|python|nikto|curl|scan|java|winhttp|HTTrack|clshttp|archiver|loader|email|harvest|extract|grab|miner)/i", $_SERVER['HTTP_USER_AGENT'])) { 213 | Response::error('Permission denied!', 403); 214 | } 215 | // Block Fake google bot 216 | self::blockFakeGoogleBots(); 217 | } 218 | 219 | /** 220 | * Sanitize from HTML injection. 221 | * 222 | * @param $data mixed data to sanitize 223 | * 224 | * @return $data sanitized data 225 | */ 226 | public static function sanitizeHtmlentities($data) 227 | { 228 | if (is_array($data)) { 229 | foreach ($data as $k => $v) { 230 | $data[$k] = self::sanitizeHtmlentities($v); 231 | } 232 | } else { 233 | $data = htmlentities($data); 234 | } 235 | 236 | return $data; 237 | } 238 | 239 | /** 240 | * Prevent Fake Google Bots. 241 | */ 242 | protected static function blockFakeGoogleBots() 243 | { 244 | $user_agent = (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''); 245 | if (preg_match('/googlebot/i', $user_agent, $matches)) { 246 | $ip = self::getIPAddress(); 247 | $name = gethostbyaddr($ip); 248 | $host_ip = gethostbyname($name); 249 | if (preg_match('/googlebot/i', $name, $matches)) { 250 | if ($host_ip != $ip) { 251 | Response::error('Permission denied!', 403); 252 | } 253 | } else { 254 | Response::error('Permission denied!', 403); 255 | } 256 | } 257 | } 258 | 259 | /** 260 | * Get IP Address. 261 | * 262 | * @return mixed 263 | */ 264 | public static function getIPAddress() 265 | { 266 | foreach ( 267 | [ 268 | 'HTTP_CLIENT_IP', 269 | 'HTTP_CF_CONNECTING_IP', 270 | 'HTTP_X_FORWARDED_FOR', 271 | 'HTTP_X_FORWARDED', 272 | 'HTTP_X_CLUSTER_CLIENT_IP', 273 | 'HTTP_FORWARDED_FOR', 274 | 'HTTP_FORWARDED', 275 | 'HTTP_VIA', 276 | 'REMOTE_ADDR', 277 | ] as $key 278 | ) { 279 | if (array_key_exists($key, $_SERVER) === true) { 280 | foreach (explode(',', $_SERVER[$key]) as $ip) { 281 | $ip = trim($ip); 282 | // Check for IPv4 IP cast as IPv6 283 | if (preg_match('/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $matches)) { 284 | $ip = $matches[1]; 285 | } 286 | if ($ip === '::1') { 287 | $ip = '127.0.0.1'; 288 | } 289 | if ($ip === '127.0.0.1' || self::isPrivateIP($ip)) { 290 | $ip = $_SERVER['REMOTE_ADDR']; 291 | if ($ip === '::1') { 292 | $ip = '127.0.0.1'; 293 | } 294 | 295 | return $ip; 296 | } 297 | if (self::validateIPAddress($ip)) { 298 | return $ip; 299 | } 300 | } 301 | } 302 | } 303 | 304 | return '0.0.0.0'; 305 | } 306 | 307 | /** 308 | * Detect if is private IP. 309 | * 310 | * @param $ip 311 | * 312 | * @return bool 313 | */ 314 | private static function isPrivateIP($ip) 315 | { 316 | // Dealing with ipv6, so we can simply rely on filter_var 317 | if (false === strpos($ip, '.')) { 318 | return !@filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); 319 | } 320 | 321 | $long_ip = ip2long($ip); 322 | // Dealing with ipv4 323 | $private_ip4_addresses = [ 324 | '10.0.0.0|10.255.255.255', // single class A network 325 | '172.16.0.0|172.31.255.255', // 16 contiguous class B network 326 | '192.168.0.0|192.168.255.255', // 256 contiguous class C network 327 | '169.254.0.0|169.254.255.255', // Link-local address also referred to as Automatic Private IP Addressing 328 | '127.0.0.0|127.255.255.255', // localhost 329 | ]; 330 | if (-1 != $long_ip) { 331 | foreach ($private_ip4_addresses as $pri_addr) { 332 | list($start, $end) = explode('|', $pri_addr); 333 | if ($long_ip >= ip2long($start) && $long_ip <= ip2long($end)) { 334 | return true; 335 | } 336 | } 337 | } 338 | 339 | return false; 340 | } 341 | 342 | /** 343 | * Ensures an ip address is both a valid IP and does not fall within 344 | * a private network range. 345 | */ 346 | public static function validateIPAddress($ip) 347 | { 348 | if (strtolower($ip) === 'unknown') { 349 | return false; 350 | } 351 | 352 | // generate ipv4 network address 353 | $ip = ip2long($ip); 354 | 355 | // if the ip is set and not equivalent to 255.255.255.255 356 | if ($ip !== false && $ip !== -1) { 357 | // make sure to get unsigned long representation of ip 358 | // due to discrepancies between 32 and 64 bit OSes and 359 | // signed numbers (ints default to signed in PHP) 360 | $ip = sprintf('%u', $ip); 361 | // do private network range checking 362 | if ($ip >= 0 && $ip <= 50331647) { 363 | return false; 364 | } 365 | if ($ip >= 167772160 && $ip <= 184549375) { 366 | return false; 367 | } 368 | if ($ip >= 2130706432 && $ip <= 2147483647) { 369 | return false; 370 | } 371 | if ($ip >= 2851995648 && $ip <= 2852061183) { 372 | return false; 373 | } 374 | if ($ip >= 2886729728 && $ip <= 2887778303) { 375 | return false; 376 | } 377 | if ($ip >= 3221225984 && $ip <= 3221226239) { 378 | return false; 379 | } 380 | if ($ip >= 3232235520 && $ip <= 3232301055) { 381 | return false; 382 | } 383 | if ($ip >= 4294967040) { 384 | return false; 385 | } 386 | } 387 | 388 | return true; 389 | } 390 | 391 | /** 392 | * Check if clients use Tor. 393 | */ 394 | public static function blockTor() 395 | { 396 | $ips = self::getAllIPAddress(); 397 | $ip_server = gethostbyname($_SERVER['SERVER_NAME']); 398 | foreach ($ips as $ip) { 399 | $query = [ 400 | implode('.', array_reverse(explode('.', $ip))), 401 | $_SERVER['SERVER_PORT'], 402 | implode('.', array_reverse(explode('.', $ip_server))), 403 | 'ip-port.exitlist.torproject.org', 404 | ]; 405 | $torExitNode = implode('.', $query); 406 | $dns = dns_get_record($torExitNode, DNS_A); 407 | if (array_key_exists(0, $dns) && array_key_exists('ip', $dns[0])) { 408 | if ($dns[0]['ip'] == '127.0.0.2') { 409 | Response::error('Permission denied!', 403); 410 | } 411 | } 412 | } 413 | } 414 | 415 | /** 416 | * Get all client IP Address. 417 | * 418 | * @return array 419 | */ 420 | public static function getAllIPAddress() 421 | { 422 | $ips = []; 423 | foreach ( 424 | [ 425 | 'GD_PHP_HANDLER', 426 | 'HTTP_AKAMAI_ORIGIN_HOP', 427 | 'HTTP_CF_CONNECTING_IP', 428 | 'HTTP_CLIENT_IP', 429 | 'HTTP_FASTLY_CLIENT_IP', 430 | 'HTTP_FORWARDED', 431 | 'HTTP_FORWARDED_FOR', 432 | 'HTTP_INCAP_CLIENT_IP', 433 | 'HTTP_TRUE_CLIENT_IP', 434 | 'HTTP_X_CLIENTIP', 435 | 'HTTP_X_CLUSTER_CLIENT_IP', 436 | 'HTTP_X_FORWARDED', 437 | 'HTTP_X_FORWARDED_FOR', 438 | 'HTTP_X_IP_TRAIL', 439 | 'HTTP_X_REAL_IP', 440 | 'HTTP_X_VARNISH', 441 | 'HTTP_VIA', 442 | 'REMOTE_ADDR', 443 | ] as $key 444 | ) { 445 | if (array_key_exists($key, $_SERVER) === true) { 446 | foreach (explode(',', $_SERVER[$key]) as $ip) { 447 | $ip = trim($ip); 448 | // Check for IPv4 IP cast as IPv6 449 | if (preg_match('/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $matches)) { 450 | $ip = $matches[1]; 451 | } 452 | if ($ip === '::1') { 453 | $ips[] = '127.0.0.1'; 454 | } elseif (self::validateIPAddress($ip)) { 455 | $ips[] = $ip; 456 | } 457 | } 458 | } 459 | } 460 | if (empty($ips)) { 461 | $ips = ['0.0.0.0']; 462 | } 463 | $ips = array_unique($ips); 464 | 465 | return $ips; 466 | } 467 | 468 | /** 469 | * Sanitize the parameters. 470 | * 471 | * @param $params mixed data to sanitize 472 | * 473 | * @return $params sanitized data 474 | */ 475 | private static function sanitizeParams($params) 476 | { 477 | foreach ($params as $key => $value) { 478 | $value = self::sanitizeRXSS($value); 479 | $value = self::sanitizeStriptags($value); 480 | $value = self::sanitizeHtmlentities($value); 481 | $value = self::sanitizeStripslashes($value); 482 | $params[$key] = $value; 483 | } 484 | 485 | return $params; 486 | } 487 | 488 | /** 489 | * Sanitize from XSS injection. 490 | * 491 | * @param $data mixed data to sanitize 492 | * 493 | * @return $data sanitized data 494 | */ 495 | public static function sanitizeRXSS($data) 496 | { 497 | if (is_array($data)) { 498 | foreach ($data as $k => $v) { 499 | $data[$k] = self::sanitizeRXSS($v); 500 | } 501 | } else { 502 | $data = self::sanitizeXSS($data); 503 | } 504 | 505 | return $data; 506 | } 507 | 508 | /** 509 | * Sanitize from XSS injection. 510 | * 511 | * @param $data mixed data to sanitize 512 | * 513 | * @return $data sanitized data 514 | */ 515 | private static function sanitizeXSS($data) 516 | { 517 | $data = str_replace(['&', '<', '>'], ['&amp;', '&lt;', '&gt;'], $data); 518 | $data = preg_replace("/(&#*\w+)[- ]+;/u", '$1;', $data); 519 | $data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data); 520 | $data = html_entity_decode($data, ENT_COMPAT, 'UTF-8'); 521 | $data = preg_replace('#(<[^>]+?[- "\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data); 522 | $data = preg_replace('#([a-z]*)[- ]*=[- ]*([`\'"]*)[- ]*j[- ]*a[- ]*v[- ]*a[- ]*s[- ]*c[- ]*r[- ]*i[- ]*p[- ]*t[- ]*:#iu', '$1=$2nojavascript', $data); 523 | $data = preg_replace('#([a-z]*)[- ]*=([\'"]*)[- ]*v[- ]*b[- ]*s[- ]*c[- ]*r[- ]*i[- ]*p[- ]*t[- ]*:#iu', '$1=$2novbscript', $data); 524 | $data = preg_replace('#([a-z]*)[- ]*=([\'"]*)[- ]*-moz-binding[- ]*:#u', '$1=$2nomozbinding', $data); 525 | $data = preg_replace('#(<[^>]+?)style[- ]*=[- ]*[`\'"]*.*?expression[- ]*\([^>]*+>#i', '$1>', $data); 526 | $data = preg_replace('#(<[^>]+?)style[- ]*=[- ]*[`\'"]*.*?behaviour[- ]*\([^>]*+>#i', '$1>', $data); 527 | $data = preg_replace('#(<[^>]+?)style[- ]*=[- ]*[`\'"]*.*?s[- ]*c[- ]*r[- ]*i[- ]*p[- ]*t[- ]*:*[^>]*+>#iu', '$1>', $data); 528 | $data = preg_replace('#]*+>#i', '', $data); 529 | do { 530 | $old_data = $data; 531 | $data = preg_replace('#]*+>#i', '', $data); 532 | } while ($old_data !== $data); 533 | $data = str_replace(chr(0), '', $data); 534 | $data = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $data); 535 | $data = str_replace('&', '&', $data); 536 | $data = preg_replace('/&#([0-9]+;)/', '&#\1', $data); 537 | $data = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $data); 538 | $data = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $data); 539 | 540 | return $data; 541 | } 542 | 543 | /** 544 | * Sanitize from HTML injection. 545 | * 546 | * @param $data mixed data to sanitize 547 | * 548 | * @return $data sanitized data 549 | */ 550 | public static function sanitizeStriptags($data) 551 | { 552 | if (is_array($data)) { 553 | foreach ($data as $k => $v) { 554 | $data[$k] = self::sanitizeStriptags($v); 555 | } 556 | } else { 557 | $data = strip_tags($data); 558 | } 559 | 560 | return $data; 561 | } 562 | 563 | /** 564 | * Sanitize from SQL injection. 565 | * 566 | * @param $data mixed data to sanitize 567 | * 568 | * @return $data sanitized data 569 | */ 570 | public static function sanitizeStripslashes($data) 571 | { 572 | if (is_array($data)) { 573 | foreach ($data as $k => $v) { 574 | $data[$k] = self::sanitizeStripslashes($v); 575 | } 576 | } else { 577 | if (get_magic_quotes_gpc()) { 578 | $data = stripslashes($data); 579 | } 580 | } 581 | 582 | return $data; 583 | } 584 | 585 | /** 586 | * Returns the request referer. 587 | */ 588 | public static function referer() 589 | { 590 | return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; 591 | } 592 | 593 | /** 594 | * Detect if is console. 595 | * 596 | * @return bool 597 | */ 598 | public static function isConsole() 599 | { 600 | if (defined('STDIN')) { 601 | return true; 602 | } 603 | if (php_sapi_name() === 'cli') { 604 | return true; 605 | } 606 | if (array_key_exists('SHELL', $_ENV)) { 607 | return true; 608 | } 609 | if (empty($_SERVER['REMOTE_ADDR']) and !isset($_SERVER['HTTP_USER_AGENT']) and count($_SERVER['argv']) > 0) { 610 | return true; 611 | } 612 | if (!array_key_exists('REQUEST_METHOD', $_SERVER)) { 613 | return true; 614 | } 615 | 616 | return false; 617 | } 618 | 619 | /** 620 | * Check customer domain. 621 | * 622 | * @param array|string $customers 623 | * 624 | * @return bool 625 | */ 626 | public static function checkDomain($customers) 627 | { 628 | if (!is_array($customers)) { 629 | $customers = [$customers]; 630 | } 631 | 632 | foreach ($customers as $customer) { 633 | // Compare 634 | if ((self::cleanHost($_SERVER['SERVER_NAME']) == self::cleanHost($customer))) { 635 | return true; 636 | } 637 | } 638 | 639 | return false; 640 | } 641 | 642 | /** 643 | * Clean Host URL. 644 | * 645 | * @param $url 646 | * 647 | * @return string 648 | */ 649 | public static function cleanHost($url) 650 | { 651 | // In case scheme relative URI is passed, e.g., //www.google.com/ 652 | $input = trim($url, '/'); 653 | // If scheme not included, prepend it 654 | if (!preg_match('#^http(s)?://#', $input)) { 655 | $input = 'http://' . $input; 656 | } 657 | $urlParts = parse_url($input); 658 | // Remove www 659 | $domain = preg_replace('/^www\./', '', $urlParts['host']); 660 | // Lower case 661 | $domain = strtolower($domain); 662 | 663 | return $domain; 664 | } 665 | } 666 | 667 | $request = new Request(); 668 | -------------------------------------------------------------------------------- /includes/classes/Response.php: -------------------------------------------------------------------------------- 1 | 9 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 10 | * 11 | * @see https://github.com/marcocesarato/Database-Web-API 12 | */ 13 | class Response 14 | { 15 | public static $instance; 16 | public $input; 17 | 18 | /** 19 | * Return success response. 20 | * 21 | * @return array 22 | */ 23 | public static function success() 24 | { 25 | http_response_code(200); 26 | 27 | return ['response' => (object)['status' => 200, 'message' => 'OK']]; 28 | } 29 | 30 | /** 31 | * Return created with success response. 32 | * 33 | * @return array 34 | */ 35 | public static function created() 36 | { 37 | http_response_code(201); 38 | 39 | return ['response' => (object)['status' => 201, 'message' => 'OK']]; 40 | } 41 | 42 | /** 43 | * Return failed response. 44 | * 45 | * @return array 46 | */ 47 | public static function failed() 48 | { 49 | $message = 'Bad request'; 50 | $code = 400; 51 | 52 | self::error($message, $code); 53 | 54 | return ['response' => (object)['status' => $code, 'message' => $message]]; 55 | } 56 | 57 | /** 58 | * Return failed response. 59 | * 60 | * @param string $msg 61 | * 62 | * @return array 63 | */ 64 | public static function noPermissions($msg = '') 65 | { 66 | $message = 'No permissions ' . $msg; 67 | $code = 403; 68 | 69 | self::error($message, $code); 70 | 71 | return ['response' => (object)['status' => $code, 'message' => $message]]; 72 | } 73 | 74 | /** 75 | * Halt the program with an "Internal server error" and the specified message. 76 | * 77 | * @param string|object $error the error or a (PDO) exception object 78 | * @param int $code (optional) the error code with which to respond 79 | * @param bool $internal if true will fallback to the first endpoint available 80 | * 81 | * @return array 82 | */ 83 | public static function error($error, $code = 500, $internal = false) 84 | { 85 | $hooks = Hooks::getInstance(); 86 | if ($internal) { 87 | $hooks->do_action('endpoint'); 88 | } 89 | $hooks->do_action('public_endpoint'); 90 | $hooks->do_action('on_error', $error, $code); 91 | 92 | $api = API::getInstance(); 93 | $logger = Logger::getInstance(); 94 | if (is_object($error) && method_exists($error, 'getMessage') && method_exists($error, 'getCode')) { 95 | $message = DatabaseErrors::errorMessage($error); 96 | $results = [ 97 | 'response' => (object)['status' => 400, 'message' => $message], 98 | ]; 99 | $logger->error('ERROR_' . $code . ': ' . $error); 100 | $api->render($results); 101 | } 102 | http_response_code($code); 103 | $error = trim($error); 104 | $logger->error('ERROR_' . $code . ': ' . $error); 105 | $results = [ 106 | 'response' => (object)['status' => $code, 'message' => Request::sanitizeHtmlentities($error)], 107 | ]; 108 | $api->render($results); 109 | 110 | return ['response' => (object)['status' => $code, 'message' => $error]]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /includes/compatibility.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 7 | * 8 | * @see https://github.com/marcocesarato/Database-Web-API 9 | */ 10 | if (!function_exists('shortcode_atts')) { 11 | /** 12 | * Combine user attributes with known attributes and fill in defaults when needed. 13 | * The pairs should be considered to be all of the attributes which are 14 | * supported by the caller and given as a list. The returned attributes will 15 | * only contain the attributes in the $pairs list. 16 | * If the $atts list has unsupported attributes, then they will be ignored and 17 | * removed from the final returned list. 18 | * 19 | * @from Wordpress 20 | * 21 | * @param array $pairs entire list of supported attributes and their defaults 22 | * @param array $atts user defined attributes in shortcode tag 23 | * 24 | * @return array combined and filtered attribute list 25 | * 26 | * @since 2.5 27 | */ 28 | function shortcode_atts($pairs, $atts) 29 | { 30 | $atts = (array)$atts; 31 | $out = []; 32 | foreach ($pairs as $name => $default) { 33 | if (array_key_exists($name, $atts)) { 34 | $out[$name] = $atts[$name]; 35 | } else { 36 | $out[$name] = $default; 37 | } 38 | } 39 | 40 | return $out; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /includes/functions.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 7 | * 8 | * @see https://github.com/marcocesarato/Database-Web-API 9 | */ 10 | 11 | namespace marcocesarato\DatabaseAPI; 12 | 13 | use DOMDocument; 14 | use stdClass; 15 | 16 | /** 17 | * If an array contain others arrays. 18 | * 19 | * @param $a 20 | * 21 | * @return bool 22 | */ 23 | function is_multi_array($a) 24 | { 25 | foreach ($a as $v) { 26 | if (is_array($v)) { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | 34 | /** 35 | * Convert array to object. 36 | * 37 | * @param $array 38 | * 39 | * @return stdClass 40 | */ 41 | function array_to_object($array) 42 | { 43 | $obj = new stdClass(); 44 | foreach ($array as $k => $v) { 45 | if (strlen($k)) { 46 | if (is_array($v)) { 47 | $obj->{$k} = array_to_object($v); //RECURSION 48 | } else { 49 | $obj->{$k} = $v; 50 | } 51 | } 52 | } 53 | 54 | return $obj; 55 | } 56 | 57 | /** 58 | * Check if site run over https. 59 | * 60 | * @return bool 61 | */ 62 | function is_https() 63 | { 64 | if (isset($_SERVER['HTTP_HOST'])) { 65 | return ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443) || 66 | (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') || 67 | (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on'); 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * Site base url. 75 | * 76 | * @param $url 77 | * 78 | * @return string 79 | */ 80 | function base_url($url) 81 | { 82 | $hostname = $_SERVER['HTTP_HOST']; 83 | if (is_https()) { 84 | $protocol = 'https://'; 85 | } else { 86 | $protocol = 'http://'; 87 | } 88 | $base = ''; 89 | $doc_root = realpath(preg_replace('/' . preg_quote($_SERVER['SCRIPT_NAME'], '/') . '$/', '', $_SERVER['SCRIPT_FILENAME'])); 90 | if (realpath(__API_ROOT__) != realpath($_SERVER['DOCUMENT_ROOT'])) { 91 | $base = str_replace(realpath($_SERVER['DOCUMENT_ROOT']), '', __API_ROOT__) . '/'; 92 | } elseif (realpath(__API_ROOT__) != $doc_root) { 93 | $base = str_replace($doc_root, '', __API_ROOT__) . '/'; 94 | } 95 | $base = str_replace('\\', '/', $base); 96 | 97 | return $protocol . preg_replace('#/+#', '/', $hostname . '/' . $base . '/' . $url); 98 | } 99 | 100 | /** 101 | * Build site base url. 102 | * 103 | * @param $url 104 | * 105 | * @return string 106 | */ 107 | function build_base_url($url) 108 | { 109 | if (!empty($_GET['db'])) { 110 | $url = '/' . $_GET['db'] . '/' . $url; 111 | } 112 | if (!empty($_GET['token'])) { 113 | $url = '/' . $_GET['token'] . '/' . $url; 114 | } 115 | 116 | return base_url($url); 117 | } 118 | 119 | /** 120 | * Trim recursive. 121 | * 122 | * @param $input 123 | * 124 | * @return array|string 125 | */ 126 | function trim_all($arr, $charlist = ' ') 127 | { 128 | if (is_string($arr)) { 129 | return trim($arr, $charlist); 130 | } 131 | 132 | if (is_array($arr)) { 133 | foreach ($arr as $key => $value) { 134 | if (is_array($value)) { 135 | $result[$key] = trim_all($value, $charlist); 136 | } else { 137 | $result[$key] = trim($value, $charlist); 138 | } 139 | } 140 | 141 | return $result; 142 | } 143 | 144 | return $arr; 145 | } 146 | 147 | /** 148 | * Recursively traverses through an array to propagate SimpleXML objects. 149 | * 150 | * @param array $array the array to parse 151 | * @param object $xml the Simple XML object (must be at least a single empty node) 152 | * 153 | * @return object the Simple XML object (with array objects added) 154 | */ 155 | function object_to_xml($array, $xml) 156 | { 157 | //array of keys that will be treated as attributes, not children 158 | $attributes = ['id']; 159 | 160 | //recursively loop through each item 161 | foreach ($array as $key => $value) { 162 | //if this is a numbered array, 163 | //grab the parent node to determine the node name 164 | if (is_numeric($key)) { 165 | $key = 'result'; 166 | } 167 | 168 | //if this is an attribute, treat as an attribute 169 | if (in_array($key, $attributes)) { 170 | $xml->addAttribute($key, $value); 171 | 172 | //if this value is an object or array, add a child node and treat recursively 173 | } elseif (is_object($value) || is_array($value)) { 174 | $child = $xml->addChild($key); 175 | $child = $this->object_to_xml($value, $child); 176 | 177 | //simple key/value child pair 178 | } else { 179 | $xml->addChild($key, $value); 180 | } 181 | } 182 | 183 | return $xml; 184 | } 185 | 186 | /** 187 | * Clean up XML domdocument formatting and return as string. 188 | * 189 | * @param $xml 190 | * 191 | * @return string 192 | */ 193 | function tidy_xml($xml) 194 | { 195 | $dom = new DOMDocument(); 196 | $dom->preserveWhiteSpace = false; 197 | $dom->formatOutput = true; 198 | $dom->loadXML($xml->asXML()); 199 | 200 | return $dom->saveXML(); 201 | } 202 | 203 | /** 204 | * Prevent malicious callbacks from being used in JSONP requests. 205 | * 206 | * @param $callback 207 | * 208 | * @return bool 209 | */ 210 | function jsonp_callback_filter($callback) 211 | { 212 | // As per . 213 | if (preg_match('/[^0-9a-zA-Z\$_]|^(abstract|boolean|break|byte|case|catch|char|class|const|continue|debugger|default|delete|do|double|else|enum|export|extends|false|final|finally|float|for|function|goto|if|implements|import|in|instanceof|int|interface|long|native|new|null|package|private|protected|public|return|short|static|super|switch|synchronized|this|throw|throws|transient|true|try|typeof|var|volatile|void|while|with|NaN|Infinity|undefined)$/', $callback)) { 214 | return false; 215 | } 216 | 217 | return $callback; 218 | } 219 | 220 | /** 221 | * Disable php errors. 222 | */ 223 | function disable_php_errors() 224 | { 225 | ini_set('display_errors', 0); 226 | ini_set('display_startup_errors', 0); 227 | error_reporting(0); 228 | } 229 | 230 | /** 231 | * Enable php errors. 232 | */ 233 | function enable_php_errors() 234 | { 235 | ini_set('display_errors', 1); 236 | ini_set('display_startup_errors', 1); 237 | error_reporting(E_ALL); // E_ALL 238 | } 239 | -------------------------------------------------------------------------------- /includes/loader.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 7 | * 8 | * @see https://github.com/marcocesarato/Database-Web-API 9 | */ 10 | 11 | namespace marcocesarato\DatabaseAPI; 12 | 13 | define('__API_ROOT__', realpath(__DIR__ . '/..')); 14 | define('__API_DIR_PLUGINS__', realpath(__API_ROOT__ . '/plugins/')); 15 | define('__API_DIR_INCLUDES__', realpath(__API_ROOT__ . '/includes/')); 16 | define('__API_DIR_CLASSES__', realpath(__API_DIR_INCLUDES__ . '/classes/')); 17 | define('__API_DIR_LIBS__', realpath(__API_DIR_CLASSES__ . '/libs/')); 18 | define('__API_DIR_LOGS__', realpath(__API_ROOT__ . '/logs/')); 19 | 20 | ini_set('zend.ze1_compatibility_mode', 0); 21 | ini_set('memory_limit', '512M'); 22 | set_time_limit(3600); 23 | 24 | // Compatibility 25 | require_once __API_DIR_INCLUDES__ . '/compatibility.php'; 26 | require_once __API_DIR_INCLUDES__ . '/functions.php'; 27 | 28 | enable_php_errors(); 29 | 30 | // Libs 31 | require_once __API_DIR_CLASSES__ . '/Hooks.php'; 32 | 33 | // Classes 34 | require_once __API_DIR_CLASSES__ . '/Logger.php'; 35 | require_once __API_DIR_CLASSES__ . '/DatabaseErrors.php'; 36 | require_once __API_DIR_CLASSES__ . '/Request.php'; 37 | require_once __API_DIR_CLASSES__ . '/Response.php'; 38 | require_once __API_DIR_CLASSES__ . '/Auth.php'; 39 | require_once __API_DIR_CLASSES__ . '/API.php'; 40 | require_once __API_DIR_CLASSES__ . '/Dump.php'; 41 | 42 | // Hooks 43 | require_once __API_DIR_PLUGINS__ . '/loader.php'; 44 | 45 | // Config 46 | require_once __API_ROOT__ . '/config.php'; 47 | 48 | Logger::getInstance()->setLog(__API_DIR_LOGS__ . '/', 'log-' . date('Y-m-d') . '.log'); 49 | 50 | API::registerDatasets(unserialize(__API_DATASETS__)); 51 | -------------------------------------------------------------------------------- /includes/template/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.3 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | background-color: #fff; 34 | color: #212529; 35 | text-align: left; 36 | font-weight: 400; 37 | font-size: 1rem; 38 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 39 | line-height: 1.5; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: 0 !important; 44 | } 45 | 46 | hr { 47 | overflow: visible; 48 | box-sizing: content-box; 49 | height: 0; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | border-bottom: 0; 65 | text-decoration: underline; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | -webkit-text-decoration: underline dotted; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | vertical-align: baseline; 121 | font-size: 75%; 122 | line-height: 0; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | background-color: transparent; 135 | color: #007bff; 136 | text-decoration: none; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-size: 1em; 164 | font-family: monospace, monospace; 165 | } 166 | 167 | pre { 168 | overflow: auto; 169 | margin-top: 0; 170 | margin-bottom: 1rem; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | border-style: none; 180 | vertical-align: middle; 181 | } 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | a, 188 | area, 189 | button, 190 | [role="button"], 191 | input:not([type="range"]), 192 | label, 193 | select, 194 | summary, 195 | textarea { 196 | -ms-touch-action: manipulation; 197 | touch-action: manipulation; 198 | } 199 | 200 | table { 201 | border-collapse: collapse; 202 | } 203 | 204 | caption { 205 | padding-top: 0.75rem; 206 | padding-bottom: 0.75rem; 207 | color: #868e96; 208 | caption-side: bottom; 209 | text-align: left; 210 | } 211 | 212 | th { 213 | text-align: inherit; 214 | } 215 | 216 | label { 217 | display: inline-block; 218 | margin-bottom: .5rem; 219 | } 220 | 221 | button { 222 | border-radius: 0; 223 | } 224 | 225 | button:focus { 226 | outline: 1px dotted; 227 | outline: 5px auto -webkit-focus-ring-color; 228 | } 229 | 230 | input, 231 | button, 232 | select, 233 | optgroup, 234 | textarea { 235 | margin: 0; 236 | font-size: inherit; 237 | font-family: inherit; 238 | line-height: inherit; 239 | } 240 | 241 | button, 242 | input { 243 | overflow: visible; 244 | } 245 | 246 | button, 247 | select { 248 | text-transform: none; 249 | } 250 | 251 | button, 252 | html [type="button"], 253 | [type="reset"], 254 | [type="submit"] { 255 | -webkit-appearance: button; 256 | } 257 | 258 | button::-moz-focus-inner, 259 | [type="button"]::-moz-focus-inner, 260 | [type="reset"]::-moz-focus-inner, 261 | [type="submit"]::-moz-focus-inner { 262 | padding: 0; 263 | border-style: none; 264 | } 265 | 266 | input[type="radio"], 267 | input[type="checkbox"] { 268 | box-sizing: border-box; 269 | padding: 0; 270 | } 271 | 272 | input[type="date"], 273 | input[type="time"], 274 | input[type="datetime-local"], 275 | input[type="month"] { 276 | -webkit-appearance: listbox; 277 | } 278 | 279 | textarea { 280 | overflow: auto; 281 | resize: vertical; 282 | } 283 | 284 | fieldset { 285 | margin: 0; 286 | padding: 0; 287 | min-width: 0; 288 | border: 0; 289 | } 290 | 291 | legend { 292 | display: block; 293 | margin-bottom: .5rem; 294 | padding: 0; 295 | max-width: 100%; 296 | width: 100%; 297 | color: inherit; 298 | white-space: normal; 299 | font-size: 1.5rem; 300 | line-height: inherit; 301 | } 302 | 303 | progress { 304 | vertical-align: baseline; 305 | } 306 | 307 | [type="number"]::-webkit-inner-spin-button, 308 | [type="number"]::-webkit-outer-spin-button { 309 | height: auto; 310 | } 311 | 312 | [type="search"] { 313 | outline-offset: -2px; 314 | -webkit-appearance: none; 315 | } 316 | 317 | [type="search"]::-webkit-search-cancel-button, 318 | [type="search"]::-webkit-search-decoration { 319 | -webkit-appearance: none; 320 | } 321 | 322 | ::-webkit-file-upload-button { 323 | font: inherit; 324 | -webkit-appearance: button; 325 | } 326 | 327 | output { 328 | display: inline-block; 329 | } 330 | 331 | summary { 332 | display: list-item; 333 | cursor: pointer; 334 | } 335 | 336 | template { 337 | display: none; 338 | } 339 | 340 | [hidden] { 341 | display: none !important; 342 | } 343 | 344 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /includes/template/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.3 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /includes/template/css/main.css: -------------------------------------------------------------------------------- 1 | /* ========================================================================== 2 | Author's custom styles 3 | ========================================================================== */ 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /includes/template/footer.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /includes/template/header.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | <?php echo __API_NAME__; ?> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 |
36 |
37 |

38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /includes/template/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/includes/template/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /includes/template/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/includes/template/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /includes/template/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /includes/template/js/vendor/modernizr-2.6.1-respond-1.1.0.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.6.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-mq-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load 3 | */ 4 | ;window.Modernizr=function(a,b,c){function D(a){j.cssText=a}function E(a,b){return D(n.join(a+";")+(b||""))}function F(a,b){return typeof a===b}function G(a,b){return!!~(""+a).indexOf(b)}function H(a,b){for(var d in a){var e=a[d];if(!G(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function I(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:F(f,"function")?f.bind(d||b):f}return!1}function J(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+p.join(d+" ")+d).split(" ");return F(b,"string")||F(b,"undefined")?H(e,b):(e=(a+" "+q.join(d+" ")+d).split(" "),I(e,b,c))}function K(){e.input=function(c){for(var d=0,e=c.length;d',a,""].join(""),k.id=h,(l?k:m).innerHTML+=f,m.appendChild(k),l||(m.style.background="",g.appendChild(m)),i=c(k,a),l?k.parentNode.removeChild(k):m.parentNode.removeChild(m),!!i},z=function(b){var c=a.matchMedia||a.msMatchMedia;if(c)return c(b).matches;var d;return y("@media "+b+" { #"+h+" { position: absolute; } }",function(b){d=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle)["position"]=="absolute"}),d},A=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=F(e[d],"function"),F(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),B={}.hasOwnProperty,C;!F(B,"undefined")&&!F(B.call,"undefined")?C=function(a,b){return B.call(a,b)}:C=function(a,b){return b in a&&F(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=w.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(w.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(w.call(arguments)))};return e}),s.flexbox=function(){return J("flexWrap")},s.canvas=function(){var a=b.createElement("canvas");return!!a.getContext&&!!a.getContext("2d")},s.canvastext=function(){return!!e.canvas&&!!F(b.createElement("canvas").getContext("2d").fillText,"function")},s.webgl=function(){return!!a.WebGLRenderingContext},s.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:y(["@media (",n.join("touch-enabled),("),h,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},s.geolocation=function(){return"geolocation"in navigator},s.postmessage=function(){return!!a.postMessage},s.websqldatabase=function(){return!!a.openDatabase},s.indexedDB=function(){return!!J("indexedDB",a)},s.hashchange=function(){return A("hashchange",a)&&(b.documentMode===c||b.documentMode>7)},s.history=function(){return!!a.history&&!!history.pushState},s.draganddrop=function(){var a=b.createElement("div");return"draggable"in a||"ondragstart"in a&&"ondrop"in a},s.websockets=function(){return"WebSocket"in a||"MozWebSocket"in a},s.rgba=function(){return D("background-color:rgba(150,255,150,.5)"),G(j.backgroundColor,"rgba")},s.hsla=function(){return D("background-color:hsla(120,40%,100%,.5)"),G(j.backgroundColor,"rgba")||G(j.backgroundColor,"hsla")},s.multiplebgs=function(){return D("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(j.background)},s.backgroundsize=function(){return J("backgroundSize")},s.borderimage=function(){return J("borderImage")},s.borderradius=function(){return J("borderRadius")},s.boxshadow=function(){return J("boxShadow")},s.textshadow=function(){return b.createElement("div").style.textShadow===""},s.opacity=function(){return E("opacity:.55"),/^0.55$/.test(j.opacity)},s.cssanimations=function(){return J("animationName")},s.csscolumns=function(){return J("columnCount")},s.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";return D((a+"-webkit- ".split(" ").join(b+a)+n.join(c+a)).slice(0,-a.length)),G(j.backgroundImage,"gradient")},s.cssreflections=function(){return J("boxReflect")},s.csstransforms=function(){return!!J("transform")},s.csstransforms3d=function(){var a=!!J("perspective");return a&&"webkitPerspective"in g.style&&y("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(b,c){a=b.offsetLeft===9&&b.offsetHeight===3}),a},s.csstransitions=function(){return J("transition")},s.fontface=function(){var a;return y('@font-face {font-family:"font";src:url("https://")}',function(c,d){var e=b.getElementById("smodernizr"),f=e.sheet||e.styleSheet,g=f?f.cssRules&&f.cssRules[0]?f.cssRules[0].cssText:f.cssText||"":"";a=/src/i.test(g)&&g.indexOf(d.split(" ")[0])===0}),a},s.generatedcontent=function(){var a;return y(['#modernizr:after{content:"',l,'";visibility:hidden}'].join(""),function(b){a=b.offsetHeight>=1}),a},s.video=function(){var a=b.createElement("video"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),c.h264=a.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,"")}catch(d){}return c},s.audio=function(){var a=b.createElement("audio"),c=!1;try{if(c=!!a.canPlayType)c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),c.mp3=a.canPlayType("audio/mpeg;").replace(/^no$/,""),c.wav=a.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),c.m4a=(a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;")).replace(/^no$/,"")}catch(d){}return c},s.localstorage=function(){try{return localStorage.setItem(h,h),localStorage.removeItem(h),!0}catch(a){return!1}},s.sessionstorage=function(){try{return sessionStorage.setItem(h,h),sessionStorage.removeItem(h),!0}catch(a){return!1}},s.webworkers=function(){return!!a.Worker},s.applicationcache=function(){return!!a.applicationCache},s.svg=function(){return!!b.createElementNS&&!!b.createElementNS(r.svg,"svg").createSVGRect},s.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==r.svg},s.smil=function(){return!!b.createElementNS&&/SVGAnimate/.test(m.call(b.createElementNS(r.svg,"animate")))},s.svgclippaths=function(){return!!b.createElementNS&&/SVGClipPath/.test(m.call(b.createElementNS(r.svg,"clipPath")))};for(var L in s)C(s,L)&&(x=L.toLowerCase(),e[x]=s[L](),v.push((e[x]?"":"no-")+x));return e.input||K(),e.addTest=function(a,b){if(typeof a=="object")for(var d in a)C(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},D(""),i=k=null,function(a,b){function k(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function l(){var a=r.elements;return typeof a=="string"?a.split(" "):a}function m(a){var b=i[a[g]];return b||(b={},h++,a[g]=h,i[h]=b),b}function n(a,c,f){c||(c=b);if(j)return c.createElement(a);f||(f=m(c));var g;return f.cache[a]?g=f.cache[a].cloneNode():e.test(a)?g=(f.cache[a]=f.createElem(a)).cloneNode():g=f.createElem(a),g.canHaveChildren&&!d.test(a)?f.frag.appendChild(g):g}function o(a,c){a||(a=b);if(j)return a.createDocumentFragment();c=c||m(a);var d=c.frag.cloneNode(),e=0,f=l(),g=f.length;for(;e",f="hidden"in a,j=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){f=!0,j=!0}})();var r={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,supportsUnknownElements:j,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:q,createElement:n,createDocumentFragment:o};a.html5=r,q(b)}(this,b),e._version=d,e._prefixes=n,e._domPrefixes=q,e._cssomPrefixes=p,e.mq=z,e.hasEvent=A,e.testProp=function(a){return H([a])},e.testAllProps=J,e.testStyles=y,e.prefixed=function(a,b,c){return b?J(a,b,c):J(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+v.join(" "):""),e}(this,this.document),function(a,b,c){function d(a){return o.call(a)=="[object Function]"}function e(a){return typeof a=="string"}function f(){}function g(a){return!a||a=="loaded"||a=="complete"||a=="uninitialized"}function h(){var a=p.shift();q=1,a?a.t?m(function(){(a.t=="c"?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){a!="img"&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l={},o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};y[c]===1&&(r=1,y[c]=[],l=b.createElement(a)),a=="object"?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),a!="img"&&(r||y[c]===2?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i(b=="c"?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),p.length==1&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&o.call(a.opera)=="[object Opera]",l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return o.call(a)=="[object Array]"},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f #mq-test-1 { width: 42px; }';a.insertBefore(d,b);c=g.offsetWidth==42;a.removeChild(d);return{matches:c,media:h}}})(document); 9 | 10 | /*! Respond.js v1.1.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */ 11 | (function(e){e.respond={};respond.update=function(){};respond.mediaQueriesSupported=e.matchMedia&&e.matchMedia("only all").matches;if(respond.mediaQueriesSupported){return}var w=e.document,s=w.documentElement,i=[],k=[],q=[],o={},h=30,f=w.getElementsByTagName("head")[0]||s,g=w.getElementsByTagName("base")[0],b=f.getElementsByTagName("link"),d=[],a=function(){var D=b,y=D.length,B=0,A,z,C,x;for(;B-1,minw:F.match(/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:F.match(/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}}j()},l,r,v=function(){var z,A=w.createElement("div"),x=w.body,y=false;A.style.cssText="position:absolute;font-size:1em;width:1em";if(!x){x=y=w.createElement("body");x.style.background="none"}x.appendChild(A);s.insertBefore(x,s.firstChild);z=A.offsetWidth;if(y){s.removeChild(x)}else{x.removeChild(A)}z=p=parseFloat(z);return z},p,j=function(I){var x="clientWidth",B=s[x],H=w.compatMode==="CSS1Compat"&&B||w.body[x]||B,D={},G=b[b.length-1],z=(new Date()).getTime();if(I&&l&&z-l-1?(p||v()):1)}if(!!J){J=parseFloat(J)*(J.indexOf(y)>-1?(p||v()):1)}if(!K.hasquery||(!A||!L)&&(A||H>=C)&&(L||H<=J)){if(!D[K.media]){D[K.media]=[]}D[K.media].push(k[K.rules])}}for(var E in q){if(q[E]&&q[E].parentNode===f){f.removeChild(q[E])}}for(var E in D){var M=w.createElement("style"),F=D[E].join("\n");M.type="text/css";M.media=E;f.insertBefore(M,G.nextSibling);if(M.styleSheet){M.styleSheet.cssText=F}else{M.appendChild(w.createTextNode(F))}q.push(M)}},n=function(x,z){var y=c();if(!y){return}y.open("GET",x,true);y.onreadystatechange=function(){if(y.readyState!=4||y.status!=200&&y.status!=304){return}z(y.responseText)};if(y.readyState==4){return}y.send(null)},c=(function(){var x=false;try{x=new XMLHttpRequest()}catch(y){x=new ActiveXObject("Microsoft.XMLHTTP")}return function(){return x}})();a();respond.update=a;function t(){j(true)}if(e.addEventListener){e.addEventListener("resize",t,false)}else{if(e.attachEvent){e.attachEvent("onresize",t)}}})(this); -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/gpl-3.0.html GNU Public License 7 | * 8 | * @see https://github.com/marcocesarato/Database-Web-API 9 | */ 10 | use marcocesarato\DatabaseAPI\API; 11 | 12 | header('Access-Control-Allow-Origin: *'); 13 | header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE'); 14 | 15 | include __DIR__ . '/includes/loader.php'; 16 | API::run(); 17 | -------------------------------------------------------------------------------- /logs.php: -------------------------------------------------------------------------------- 1 | '0b14d501a594442a01c6859541bcb3e8164d183d32937b851835442f69d5c94e', // password1 9 | 'user2' => '6cf615d5bcaac778352a8f1f3360d23f02f34ec182e259897fd6ce485d7870d4', // password2 10 | ]; 11 | 12 | if (isset($_POST['username'])) { 13 | $_POST['password'] = hash('sha256', $_POST['password']); 14 | if (@$userinfo[$_POST['username']] == $_POST['password']) { 15 | $_SESSION['username'] = $_POST['username']; 16 | } 17 | } 18 | if (!isset($_SESSION['username'])) { 19 | echo << 21 | 22 | 23 | API Logs - Login 24 | 25 | 26 | 27 | 28 |
29 |

API

30 |

Logs - Login

31 |
32 |
33 |
34 |
35 | 36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 | 50 | EOD; 51 | } 52 | 53 | echo << 55 | 56 | 57 | 58 | 59 | API Logs 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 83 | 84 | 85 | 86 |
87 |

API

88 |

Logs

89 |
90 |
91 | EOD; 92 | 93 | if (empty($_REQUEST['file'])) { 94 | $logs = scandir('logs'); 95 | 96 | echo '

Logs list

'; 97 | 98 | echo ''; 105 | } else { 106 | $_REQUEST['file'] = str_replace(['/', '..'], '', $_REQUEST['file']); 107 | 108 | if (file_exists('logs/' . $_REQUEST['file'])) { 109 | include_once '../../config.php'; 110 | $db = new PDO('pgsql:dbname=' . $dbconfig['db_name'] . ';port=' . $dbconfig['db_port'] . ';host=' . $dbconfig['db_host_name'], $dbconfig['db_user_name'], $dbconfig['db_password']); 111 | 112 | $f = file_get_contents('logs/' . $_REQUEST['file']); 113 | 114 | $type = 'new'; 115 | if (strpos($f, "\n\n") !== false) { 116 | $f = str_replace(["\n\n", "\n", "\n\n"], ['[br]', '', "\n"], $f); 117 | $type = 'old'; 118 | } 119 | 120 | $re = '/^\[(.*?)\]\s\[(.*?)\]\s\[(?:method\s)?(.*?)\]\s\[(?:url\s)?(.*?)\](?:\s\[token\s(.*?)\])?(?:\s\[client\s(.*?)\])?:(?:[\s\t])(?:([a-zA-Z]+)(\s.*))?(.*)$/m'; 121 | 122 | preg_match_all($re, $f, $matches, PREG_SET_ORDER, 0); 123 | 124 | $logs = []; 125 | $users = []; 126 | $log_types = []; 127 | $actions = []; 128 | foreach ($matches as $row) { 129 | list($full_row, $date, $log_type, $method, $url, $token, $ip, $action, $query, $message) = $row; 130 | 131 | if (!isset($logs[$token])) { 132 | $logs[$token] = []; 133 | 134 | if (!empty($token)) { 135 | $sth = $db->prepare("SELECT u.first_name || ' ' || u.last_name FROM api_authentication AS a INNER JOIN users AS u ON u.id = a.user_id WHERE a.token = :token"); 136 | $sth->bindValue(':token', $token, PDO::PARAM_STR); 137 | if ($sth->execute()) { 138 | $name = $sth->fetch(PDO::FETCH_COLUMN); 139 | } else { 140 | $name = $token; 141 | } 142 | 143 | if (!isset($users[$name])) { 144 | $users[$name] = []; 145 | } 146 | $users[$name][] = $token; 147 | } elseif (!isset($users['Vuoto'])) { 148 | $users['Vuoto'] = ['-empty-']; 149 | } 150 | } 151 | if (!isset($logs[$token][$log_type])) { 152 | $logs[$token][$log_type] = []; 153 | if (!in_array($log_type, $log_types)) { 154 | $log_types[] = $log_type; 155 | } 156 | } 157 | 158 | $action_group = (empty($action) ? 'MESSAGE' : $action); 159 | if (!isset($logs[$token][$log_type][$action_group])) { 160 | $logs[$token][$log_type][$action_group] = []; 161 | if (!in_array($action_group, $actions)) { 162 | $actions[] = $action_group; 163 | } 164 | } 165 | 166 | $log = [ 167 | 'date' => $date, 168 | 'url' => $url, 169 | 'method' => $method, 170 | ]; 171 | 172 | if (!empty($action)) { 173 | $log['query'] = $action . $query; 174 | } else { 175 | $log['message'] = $message; 176 | } 177 | 178 | $logs[$token][$log_type][$action_group][] = $log; 179 | } 180 | 181 | echo '

Select log to view

'; 182 | echo '
'; 183 | echo '
'; 184 | echo '
'; 190 | 191 | echo '
'; 192 | echo '
'; 197 | 198 | echo '
'; 199 | echo '
'; 204 | 205 | echo '
'; 206 | echo ''; 207 | echo '
'; 208 | echo ''; 209 | 210 | if (isset($_REQUEST['show'])) { 211 | $user = explode(',', $_REQUEST['user']); 212 | $log_type = $_REQUEST['type']; 213 | $action = $_REQUEST['action']; 214 | 215 | $users_new = []; 216 | foreach ($users as $username => $u) { 217 | foreach ($u as $t) { 218 | $users_new[$t] = $username; 219 | } 220 | } 221 | $users = $users_new; 222 | 223 | $users_db = []; 224 | $sth = $db->prepare("SELECT id, first_name || ' ' || last_name AS name FROM users"); 225 | if ($sth->execute()) { 226 | $users_db = $sth->fetchAll(PDO::FETCH_ASSOC); 227 | } 228 | 229 | $output = []; 230 | foreach ($logs as $t => $log) { 231 | $t = empty($t) ? '-empty-' : $t; 232 | if (empty($user[0]) || in_array($t, $user)) { 233 | foreach ($log as $lt => $types) { 234 | if (empty($log_type) || ($lt === $log_type)) { 235 | foreach ($types as $a => $actions) { 236 | if (empty($action) || ($a === $action)) { 237 | $u = (isset($users[$t]) ? $users[$t] : ''); 238 | 239 | foreach ($actions as $def) { 240 | if (isset($def['message']) && in_array(substr($def['message'], 0, 1), ['{', '['])) { 241 | $msg = json_decode($def['message'], true); 242 | 243 | if (isset($msg['user'])) { 244 | $key = array_search($msg['user']['id'], array_column($users_db, 'id')); 245 | if (!empty($key)) { 246 | $u = $users_db[$key]['name']; 247 | } 248 | } 249 | } 250 | 251 | $output_date = strtotime($def['date']); 252 | while (isset($output[$output_date])) { 253 | $output_date++; 254 | } 255 | $output[$output_date] = '
' . (empty($log_type) ? '' : '') . ''; 256 | } 257 | } 258 | } 259 | } 260 | } 261 | } 262 | } 263 | echo '

Results

'; 264 | echo '
' . $def['date'] . '' . $u . '' . $lt . '' . (isset($def['query']) ? $def['query'] : $def['message']) . '' . $def['method'] . '' . $def['url'] . '
' . (empty($log_type) ? '' : '') . ''; 265 | ksort($output, SORT_NUMERIC); 266 | echo implode('', $output); 267 | echo '
DateUserTypeQuery / MessageMethodURL
'; 268 | echo ''; 280 | } 281 | } else { 282 | echo 'File not found.'; 283 | } 284 | } 285 | 286 | echo << 288 | 289 | 290 | EOD; 291 | -------------------------------------------------------------------------------- /logs/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/logs/.empty -------------------------------------------------------------------------------- /plugins/actions.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | use marcocesarato\DatabaseAPI\API; 8 | use marcocesarato\DatabaseAPI\Response; 9 | 10 | /** 11 | * Endpoint. 12 | * 13 | * @param $query 14 | */ 15 | function action_endpoint($query) 16 | { 17 | $api = API::getInstance(); 18 | $api->query['part_1'] = $api->query['db']; 19 | $api->query['part_2'] = (!empty($api->query['limit']) ? $api->query['limit'] : $api->query['table']); 20 | $api->query['part_3'] = (!empty($api->query['id']) ? $api->query['id'] : $api->query['table']); 21 | $api->query['part_4'] = null; 22 | 23 | if (!empty($api->query['where']) && count($api->query['where']) == 1) { 24 | $api->query['part_4'] = reset($api->query['where']); 25 | $api->query['part_3'] = key($api->query['where']); 26 | } 27 | } 28 | 29 | /** 30 | * On error. 31 | * 32 | * @param $message 33 | * @param $code 34 | */ 35 | function action_on_error($message, $code) 36 | { 37 | } 38 | 39 | /** 40 | * Login Custom. 41 | * 42 | * @param $query 43 | */ 44 | /*function login_custom($query){ 45 | 46 | $auth = Auth::getInstance(); 47 | $db = API::getConnection(); // PDO Object 48 | $api = API::getInstance(); 49 | 50 | $user = strtolower($query['user_id']); 51 | 52 | //.... 53 | 54 | if($login){ 55 | // Login 56 | $results = array((object)array( 57 | "token" => $token, 58 | "id" => $user_row['id'], 59 | "is_admin" => false, 60 | )); 61 | $auth->logger->debug($results); 62 | $api->render($results); 63 | 64 | } 65 | Response::error("Invalid authentication!", 401); 66 | }*/ 67 | -------------------------------------------------------------------------------- /plugins/custom/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/plugins/custom/.empty -------------------------------------------------------------------------------- /plugins/custom/example.hooks.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | use marcocesarato\DatabaseAPI\API; 8 | use marcocesarato\DatabaseAPI\Auth; 9 | use marcocesarato\DatabaseAPI\Hooks; 10 | 11 | $hooks = Hooks::getInstance(); 12 | 13 | /** 14 | * Endpoint example. 15 | * 16 | * @return mixed or die (with mixed return just skip to next action until 404 error) 17 | */ 18 | function action_endpoint_example() 19 | { 20 | $user = Auth::getUser(); // User row 21 | $api = API::getInstance(); // PDO Object 22 | $db = API::getConnection('dataset'); // $db MUST NOT BE EMPTY or WILL CAUSE A LOOP 23 | 24 | // Example url: example.com/TOKEN/part_1/part_2/part_3.format 25 | $part_1 = $api->query['part_1']; 26 | $part_2 = $api->query['part_2']; 27 | $part_3 = $api->query['part_3']; 28 | 29 | // example.com/TOKEN/example/something.json 30 | /*if($part_1 == 'example') { 31 | $example = new StdClass(); 32 | $example->id = '1'; 33 | $example->desc = "Example custom call"; 34 | $api->render(array($example, $example, $example)); 35 | }*/ 36 | } 37 | 38 | // Private 39 | $hooks->add_action('endpoint', 'action_endpoint_example'); 40 | // Public 41 | $hooks->add_action('public_endpoint', 'action_endpoint_example'); 42 | -------------------------------------------------------------------------------- /plugins/filters.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | use marcocesarato\DatabaseAPI\API; 8 | use marcocesarato\DatabaseAPI\Auth; 9 | use marcocesarato\DatabaseAPI\Hooks; 10 | use marcocesarato\DatabaseAPI\Request; 11 | 12 | /** 13 | * On read loader hooks. 14 | * 15 | * @param $data 16 | * @param $table 17 | * 18 | * @return mixed 19 | */ 20 | function loader_on_read_tables($data, $table) 21 | { 22 | $hooks = Hooks::getInstance(); 23 | $data = $hooks->apply_filters('on_read_' . $table, $data, $table); 24 | 25 | return $data; 26 | } 27 | 28 | /** 29 | * On write loader hooks. 30 | * 31 | * @param $data 32 | * @param $table 33 | * 34 | * @return mixed 35 | */ 36 | function loader_on_write_tables($data, $table) 37 | { 38 | $hooks = Hooks::getInstance(); 39 | $data = $hooks->apply_filters('on_write_' . $table, $data, $table); 40 | 41 | return $data; 42 | } 43 | 44 | /** 45 | * On edit loader hooks. 46 | * 47 | * @param $data 48 | * @param $table 49 | * 50 | * @return mixed 51 | */ 52 | function loader_on_edit_tables($data, $table) 53 | { 54 | $hooks = Hooks::getInstance(); 55 | $data = $hooks->apply_filters('on_edit_' . $table, $data, $table); 56 | 57 | return $data; 58 | } 59 | 60 | /** 61 | * Add restriction on where conditions for each query. 62 | * 63 | * @param string $restriction 64 | * @param string $table 65 | * @param bool $permission 66 | * 67 | * @return string 68 | */ 69 | function filter_sql_restriction($restriction, $table, $permission) 70 | { 71 | return $restriction; // Continue or return $sql 72 | } 73 | 74 | /** 75 | * Return if can select. 76 | * 77 | * @param bool $permission 78 | * @param string $table 79 | * 80 | * @return bool 81 | */ 82 | function filter_can_read($permission, $table) 83 | { 84 | $user = Auth::getUser(); // User row 85 | $db = API::getConnection(); // PDO Object 86 | 87 | return $permission; 88 | } 89 | 90 | /** 91 | * Return if can insert. 92 | * 93 | * @param bool $permission 94 | * @param string $table 95 | * 96 | * @return bool 97 | */ 98 | function filter_can_write($permission, $table) 99 | { 100 | $user = Auth::getUser(); // User row 101 | $db = API::getConnection(); // PDO Object 102 | 103 | return $permission; 104 | } 105 | 106 | /** 107 | * Return if can update. 108 | * 109 | * @param bool $permission 110 | * @param string $table 111 | * 112 | * @return bool 113 | */ 114 | function filter_can_edit($permission, $table) 115 | { 116 | $user = Auth::getUser(); // User row 117 | $db = API::getConnection(); // PDO Object 118 | 119 | return $permission; 120 | } 121 | 122 | /** 123 | * Return if can delete. 124 | * 125 | * @param bool $permission 126 | * @param string $table 127 | * 128 | * @return bool 129 | */ 130 | function filter_can_delete($permission, $table) 131 | { 132 | $user = Auth::getUser(); // User row 133 | $db = API::getConnection(); // PDO Object 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * On read. 140 | * 141 | * @param object $data 142 | * @param string $table 143 | * 144 | * @return object 145 | */ 146 | function filter_on_read($data, $table) 147 | { 148 | $user = Auth::getUser(); // User row 149 | $db = API::getConnection(); // PDO Object 150 | 151 | return $data; 152 | } 153 | 154 | /** 155 | * On write. 156 | * 157 | * @param object $data 158 | * @param string $table 159 | * 160 | * @return object 161 | */ 162 | function filter_on_write($data, $table) 163 | { 164 | $user = Auth::getUser(); // User row 165 | $db = API::getConnection(); // PDO Object 166 | 167 | return $data; 168 | } 169 | 170 | /** 171 | * On write already exists. 172 | * 173 | * @param bool $pass 174 | * @param object $item 175 | * @param string $table 176 | * 177 | * @return bool 178 | */ 179 | function filter_on_write_exists($bypass, $item, $table) 180 | { 181 | return $bypass; 182 | } 183 | 184 | /** 185 | * On edit. 186 | * 187 | * @param object $data 188 | * @param string $table 189 | * 190 | * @return object 191 | */ 192 | function filter_on_edit($data, $table) 193 | { 194 | $user = Auth::getUser(); // User row 195 | $db = API::getConnection(); // PDO Object 196 | 197 | return $data; 198 | } 199 | 200 | /** 201 | * Validate token. 202 | * 203 | * @param $is_valid 204 | * @param $token 205 | * 206 | * @return bool 207 | */ 208 | function filter_auth_validate_token($is_valid, $token) 209 | { 210 | return $is_valid; 211 | } 212 | 213 | /** 214 | * Validate authentication. 215 | * 216 | * @param bool $is_valid 217 | * @param array $user_row 218 | * 219 | * @return bool 220 | */ 221 | function filter_auth_validate_user($is_valid, $user_row) 222 | { 223 | return $is_valid; 224 | } 225 | 226 | /** 227 | * Filter user auth login. 228 | * 229 | * @param string|int $user_id 230 | * 231 | * @return string 232 | */ 233 | function filter_auth_user_id($user_id) 234 | { 235 | return $user_id; 236 | } 237 | 238 | /** 239 | * Bypass authentication. 240 | * 241 | * @param bool $bypass 242 | * 243 | * @return bool 244 | */ 245 | function filter_auth_bypass($bypass) 246 | { 247 | $ip = Request::getIPAddress(); 248 | 249 | //return in_array($ip, array('heartquarter' => '0.0.0.0')); 250 | return $bypass; 251 | } 252 | 253 | /** 254 | * Check if is a login request and return login action. 255 | * 256 | * @param bool $is_valid_request 257 | * @param string $query 258 | * 259 | * @return string|false 260 | */ 261 | function filter_auth_login_request($is_valid_request, $query) 262 | { 263 | $hooks = Hooks::getInstance(); 264 | 265 | /*if(isset($query['user_id']) && $query['user_id'] != 'admin' && isset($query['password']) && !empty($query['client_id']) && $query['referer'] == "login_custom") { 266 | $hooks->add_action('login_custom','action_login_custom'); 267 | return "login_custom"; 268 | }*/ 269 | 270 | return $is_valid_request; 271 | } 272 | 273 | /** 274 | * Login data result. 275 | * 276 | * @param array $data 277 | * 278 | * @return array 279 | */ 280 | function filter_auth_login($data) 281 | { 282 | $user = Auth::getUser(); // User row 283 | $db = API::getConnection(); // PDO Object 284 | 285 | return $data; 286 | } 287 | 288 | /** 289 | * Token check data result. 290 | * 291 | * @param array $data 292 | * 293 | * @return array 294 | */ 295 | function filter_auth_token_check($data) 296 | { 297 | $user = Auth::getUser(); // User row 298 | $db = API::getConnection(); // PDO Object 299 | 300 | return $data; 301 | } 302 | 303 | /** 304 | * Render. 305 | * 306 | * @param $data 307 | * @param string $query 308 | * @param string $method 309 | * 310 | * @return array 311 | */ 312 | function filter_render($data, $query, $method) 313 | { 314 | switch ($method) { 315 | case 'GET': 316 | break; 317 | case 'POST': 318 | break; 319 | case 'PATCH': 320 | break; 321 | case 'PUT': 322 | break; 323 | case 'DELETE': 324 | break; 325 | } 326 | 327 | return $data; 328 | } 329 | 330 | /** 331 | * Filter GET request query table. 332 | * 333 | * @param string $table 334 | * 335 | * @return string 336 | */ 337 | function filter_get_query_table($table) 338 | { 339 | return $table; 340 | } 341 | 342 | /** 343 | * Filter GET request SELECT columns. 344 | * 345 | * @param array $columns 346 | * 347 | * @return array 348 | */ 349 | function filter_selected_columns($columns) 350 | { 351 | return $columns; 352 | } 353 | 354 | /** 355 | * Filter GET request JOIN ON. 356 | * 357 | * @param string $on 358 | * @param string $join_on_table 359 | * 360 | * @return string 361 | */ 362 | function filter_get_query_additional_join_on($on, $join_on_table) 363 | { 364 | return $on; 365 | } 366 | 367 | /** 368 | * Filter query input request. 369 | * 370 | * @param array $query 371 | * 372 | * @return array 373 | */ 374 | function filter_request_input_query($query) 375 | { 376 | return $query; 377 | } 378 | 379 | /** 380 | * Filter GET request WHERE values. 381 | * 382 | * @param string $values 383 | * @param string $table 384 | * 385 | * @return string 386 | */ 387 | function filter_get_where_values($values, $table) 388 | { 389 | return $values; 390 | } 391 | 392 | /** 393 | * Filter GET request additional WHERE conditions. 394 | * 395 | * @param string $where 396 | * @param string $table 397 | * 398 | * @return string 399 | */ 400 | function filter_get_query_additional_where($where, $table) 401 | { 402 | return $where; 403 | } 404 | 405 | /** 406 | * Filter PATCH request WHERE values. 407 | * 408 | * @param string $values 409 | * @param string $table 410 | * 411 | * @return string 412 | */ 413 | function filter_patch_where_values($values, $table) 414 | { 415 | return $values; 416 | } 417 | 418 | /** 419 | * Filter DELETE request WHERE values. 420 | * 421 | * @param string $values 422 | * @param string $table 423 | * 424 | * @return string 425 | */ 426 | function filter_delete_where_values($values, $table) 427 | { 428 | return $values; 429 | } 430 | -------------------------------------------------------------------------------- /plugins/functions.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | function func_util_example($res) 8 | { 9 | return $res; 10 | } 11 | -------------------------------------------------------------------------------- /plugins/helpers/example.hooks.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | use marcocesarato\DatabaseAPI\API; 9 | use marcocesarato\DatabaseAPI\Auth; 10 | use marcocesarato\DatabaseAPI\Hooks; 11 | use marcocesarato\DatabaseAPI\Response; 12 | 13 | $hooks = Hooks::getInstance(); 14 | 15 | /** 16 | * On write helper hooks. 17 | * 18 | * @param $data 19 | * @param $table 20 | * 21 | * @return mixed 22 | */ 23 | function helper_on_write($data, $table) 24 | { 25 | $user = Auth::getUser(); // User row 26 | $api = API::getInstance(); 27 | 28 | /*if($api->checkColumn('date_entered', $table)) { 29 | $data['date_entered'] = date('Y-m-d'); 30 | } 31 | if($api->checkColumn('assigned_user_id', $table)) { 32 | $data['assigned_user_id'] = $user['id']; 33 | }; 34 | if($api->checkColumn('created_by', $table)) { 35 | $data['created_by'] = $user['id']; 36 | } 37 | if($api->checkColumn('modified_user_id', $table)) { 38 | $data['modified_user_id'] = $user['id']; 39 | } 40 | if($api->checkColumn('date_modified', $table)) { 41 | $data['date_modified'] = date('Y-m-d'); 42 | }*/ 43 | 44 | return $data; 45 | } 46 | 47 | $hooks->add_filter('on_write', 'helper_on_write', 100); 48 | 49 | /** 50 | * On edit helper hooks. 51 | * 52 | * @param $data 53 | * @param $table 54 | * 55 | * @return mixed 56 | */ 57 | function helper_on_edit($data, $table) 58 | { 59 | $user = Auth::getUser(); // User row 60 | $api = API::getInstance(); 61 | 62 | /*if($api->checkColumn('modified_user_id', $table)) { 63 | $data['modified_user_id'] = $user['id']; 64 | } 65 | if($api->checkColumn('date_modified', $table)) { 66 | $data['date_modified'] = date('Y-m-d'); 67 | }*/ 68 | 69 | return $data; 70 | } 71 | 72 | $hooks->add_filter('on_edit', 'helper_on_edit', 100); 73 | 74 | /** 75 | * Patch where values. 76 | * 77 | * @param $where_values 78 | * @param $table 79 | * 80 | * @return mixed 81 | */ 82 | function helper_patch_where_values($where_values, $table) 83 | { 84 | if (empty($where_values)) { 85 | Response::error('Invalid update condition', 404); 86 | } 87 | 88 | return $where_values; 89 | } 90 | 91 | $hooks->add_filter('patch_where_values', 'helper_patch_where_values', 100); 92 | 93 | /** 94 | * Delete where values. 95 | * 96 | * @param $where_values 97 | * @param $table 98 | * 99 | * @return mixed 100 | */ 101 | function helper_delete_where_values($where_values, $table) 102 | { 103 | if (empty($where_values)) { 104 | Response::error('Invalid delete condition', 404); 105 | } 106 | 107 | return $where_values; 108 | } 109 | 110 | $hooks->add_filter('delete_where_values', 'helper_delete_where_values', 100); 111 | -------------------------------------------------------------------------------- /plugins/loader.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | use marcocesarato\DatabaseAPI\Hooks; 8 | 9 | require_once __API_DIR_PLUGINS__ . '/functions.php'; 10 | require_once __API_DIR_PLUGINS__ . '/filters.php'; 11 | require_once __API_DIR_PLUGINS__ . '/actions.php'; 12 | 13 | $hooks = Hooks::getInstance(); 14 | 15 | // Include plugins 16 | $dir = new RecursiveDirectoryIterator(__API_DIR_PLUGINS__); 17 | $ite = new RecursiveIteratorIterator($dir); 18 | $pattern = '/^.+\/([^\/]+)\/([^\/]+)\.hooks\.php$/'; 19 | $files = new RegexIterator($ite, $pattern, RegexIterator::GET_MATCH); 20 | foreach ($files as $file) { 21 | include_once $file[0]; 22 | } 23 | 24 | // Register loaders 25 | $hooks->add_filter('on_read', 'loader_on_read_tables', 25); 26 | $hooks->add_filter('on_write', 'loader_on_write_tables', 25); 27 | $hooks->add_filter('on_edit', 'loader_on_edit_tables', 25); 28 | 29 | // Register filters and actions 30 | $hooks->add_action('endpoint', 'action_endpoint', 1); 31 | $hooks->add_action('public_endpoint', 'action_endpoint', 1); 32 | $hooks->add_action('on_error', 'action_on_error'); 33 | $hooks->add_filter('request_input_query', 'filter_request_input_query'); 34 | $hooks->add_filter('sql_restriction', 'filter_sql_restriction'); 35 | $hooks->add_filter('can_read', 'filter_can_read'); 36 | $hooks->add_filter('can_write', 'filter_can_write'); 37 | $hooks->add_filter('can_edit', 'filter_can_edit'); 38 | $hooks->add_filter('can_delete', 'filter_can_delete'); 39 | $hooks->add_filter('on_read', 'filter_on_read'); 40 | $hooks->add_filter('on_write', 'filter_on_write'); 41 | $hooks->add_filter('on_write_exists', 'filter_on_write_exists'); 42 | $hooks->add_filter('on_edit', 'filter_on_edit'); 43 | $hooks->add_filter('auth_validate_user', 'filter_auth_validate_user'); 44 | $hooks->add_filter('auth_validate_token', 'filter_auth_validate_token'); 45 | $hooks->add_filter('auth_user_id', 'filter_auth_user_id'); 46 | $hooks->add_filter('auth_bypass', 'filter_auth_bypass'); 47 | $hooks->add_filter('auth_login', 'filter_auth_login'); 48 | $hooks->add_filter('render', 'filter_render'); 49 | $hooks->add_filter('get_query_table', 'filter_get_query_table'); 50 | $hooks->add_filter('selected_columns', 'filter_selected_columns'); 51 | $hooks->add_filter('get_query_additional_join_on', 'filter_get_query_additional_join_on'); 52 | $hooks->add_filter('get_where_values', 'filter_get_where_values'); 53 | $hooks->add_filter('get_patch_values', 'filter_patch_where_values'); 54 | $hooks->add_filter('get_delete_values', 'filter_delete_where_values'); 55 | $hooks->add_filter('get_query_additional_where', 'filter_get_query_additional_where'); 56 | -------------------------------------------------------------------------------- /plugins/tables/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcocesarato/Database-Web-API/a09145a560c7232b1c5d9a661d027913ab237d9c/plugins/tables/.empty -------------------------------------------------------------------------------- /plugins/tables/example.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | use marcocesarato\DatabaseAPI\API; 8 | use marcocesarato\DatabaseAPI\Hooks; 9 | 10 | $hooks = Hooks::getInstance(); 11 | 12 | /** 13 | * On write example table (POST/PUT request). 14 | * 15 | * @param $data 16 | * 17 | * @return mixed 18 | */ 19 | function filter_on_write_example($data, $table) 20 | { 21 | $db = API::getConnection(); // PDO Object 22 | /* 23 | $data['uuid'] = uniqid(); 24 | $data['timestamp'] = time(); 25 | */ 26 | 27 | return $data; 28 | } 29 | 30 | $hooks->add_filter('on_write_example', 'filter_on_write_example'); 31 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 |