├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── composer.lock └── src └── Tqdev └── PhpCrudApi ├── Api.php ├── Cache ├── Base │ └── BaseCache.php ├── Cache.php ├── CacheFactory.php ├── MemcacheCache.php ├── MemcachedCache.php ├── NoCache.php ├── RedisCache.php └── TempFileCache.php ├── Column ├── DefinitionService.php ├── Reflection │ ├── ReflectedColumn.php │ ├── ReflectedDatabase.php │ └── ReflectedTable.php └── ReflectionService.php ├── Config ├── Base │ └── ConfigInterface.php └── Config.php ├── Controller ├── CacheController.php ├── ColumnController.php ├── GeoJsonController.php ├── JsonResponder.php ├── OpenApiController.php ├── RecordController.php ├── Responder.php └── StatusController.php ├── Database ├── ColumnConverter.php ├── ColumnsBuilder.php ├── ConditionsBuilder.php ├── DataConverter.php ├── GenericDB.php ├── GenericDefinition.php ├── GenericReflection.php ├── LazyPdo.php ├── RealNameMapper.php └── TypeConverter.php ├── GeoJson ├── Feature.php ├── FeatureCollection.php ├── GeoJsonService.php └── Geometry.php ├── Middleware ├── AjaxOnlyMiddleware.php ├── ApiKeyAuthMiddleware.php ├── ApiKeyDbAuthMiddleware.php ├── AuthorizationMiddleware.php ├── Base │ └── Middleware.php ├── BasicAuthMiddleware.php ├── Communication │ └── VariableStore.php ├── CorsMiddleware.php ├── CustomizationMiddleware.php ├── DbAuthMiddleware.php ├── FirewallMiddleware.php ├── IpAddressMiddleware.php ├── JoinLimitsMiddleware.php ├── JsonMiddleware.php ├── JwtAuthMiddleware.php ├── MultiTenancyMiddleware.php ├── PageLimitsMiddleware.php ├── ReconnectMiddleware.php ├── Router │ ├── Router.php │ └── SimpleRouter.php ├── SanitationMiddleware.php ├── SslRedirectMiddleware.php ├── TextSearchMiddleware.php ├── ValidationMiddleware.php ├── WpAuthMiddleware.php ├── XmlMiddleware.php └── XsrfMiddleware.php ├── OpenApi ├── OpenApiBuilder.php ├── OpenApiColumnsBuilder.php ├── OpenApiDefinition.php ├── OpenApiRecordsBuilder.php ├── OpenApiService.php └── OpenApiStatusBuilder.php ├── Record ├── ColumnIncluder.php ├── Condition │ ├── AndCondition.php │ ├── ColumnCondition.php │ ├── Condition.php │ ├── NoCondition.php │ ├── NotCondition.php │ ├── OrCondition.php │ └── SpatialCondition.php ├── Document │ ├── ErrorDocument.php │ └── ListDocument.php ├── ErrorCode.php ├── FilterInfo.php ├── HabtmValues.php ├── OrderingInfo.php ├── PaginationInfo.php ├── PathTree.php ├── RecordService.php └── RelationJoiner.php ├── RequestFactory.php ├── RequestUtils.php ├── ResponseFactory.php └── ResponseUtils.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to php-crud-api 2 | 3 | Pull requests are welcome. 4 | 5 | ## Use phpfmt 6 | 7 | Please use "phpfmt" to ensure consistent formatting. 8 | 9 | ## Run the tests 10 | 11 | Before you do a PR, you should ensure any new functionality has test cases and that all existing tests are succeeding. 12 | 13 | ## Run the build 14 | 15 | Since this project is a single file application, you must ensure that classes are loaded in the correct order. 16 | This is only important for the "extends" and "implements" relations. The 'build.php' script appends the classes in 17 | alphabetical order (directories first). The path of the class that is extended or implemented (parent) must be above 18 | the extending or implementing (child) class when listing the contents of the 'src' directory in this order. If you 19 | get this order wrong you will see the build will fail with a "Class not found" error message. The solution is to 20 | rename the child class so that it starts with a later letter in the alphabet than the parent class or that you move 21 | the parent class to a subdirectory (directories are scanned first). 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Maurits van der Schee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mevdschee/php-crud-api", 3 | "type": "library", 4 | "description": "Single file PHP script that adds a REST API to a SQL database.", 5 | "keywords": [ 6 | "api-server", 7 | "restful", 8 | "mysql", 9 | "geospatial", 10 | "php", 11 | "sql-server", 12 | "postgresql", 13 | "php-api", 14 | "postgis", 15 | "crud", 16 | "rest-api", 17 | "openapi", 18 | "swagger", 19 | "automatic-api", 20 | "database", 21 | "multi-database", 22 | "sql-database", 23 | "ubuntu-linux" 24 | ], 25 | "homepage": "https://github.com/mevdschee/php-crud-api", 26 | "license": "MIT", 27 | "authors": [ 28 | { 29 | "name": "Maurits van der Schee", 30 | "email": "maurits@vdschee.nl", 31 | "homepage": "https://github.com/mevdschee" 32 | } 33 | ], 34 | "require": { 35 | "php": ">=7.0.0", 36 | "ext-zlib": "*", 37 | "ext-json": "*", 38 | "ext-pdo": "*", 39 | "ext-mbstring": "*", 40 | "psr/http-message": "*", 41 | "psr/http-factory": "*", 42 | "psr/http-server-handler": "*", 43 | "psr/http-server-middleware": "*", 44 | "nyholm/psr7": "*", 45 | "nyholm/psr7-server": "*" 46 | }, 47 | "suggest": { 48 | "ext-memcache": "*", 49 | "ext-memcached": "*", 50 | "ext-redis": "*" 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "Tqdev\\PhpCrudApi\\": "src/Tqdev/PhpCrudApi" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Cache/Base/BaseCache.php: -------------------------------------------------------------------------------- 1 | get('__ping__'); 32 | return intval((microtime(true)-$start)*1000000); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Cache/Cache.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 15 | if ($config == '') { 16 | $address = 'localhost'; 17 | $port = 11211; 18 | } elseif (strpos($config, ':') === false) { 19 | $address = $config; 20 | $port = 11211; 21 | } else { 22 | list($address, $port) = explode(':', $config); 23 | } 24 | $this->memcache = $this->create(); 25 | $this->memcache->addServer($address, $port); 26 | } 27 | 28 | protected function create() /*: \Memcache*/ 29 | { 30 | return new \Memcache(); 31 | } 32 | 33 | public function set(string $key, string $value, int $ttl = 0): bool 34 | { 35 | return $this->memcache->set($this->prefix . $key, $value, 0, $ttl); 36 | } 37 | 38 | public function get(string $key): string 39 | { 40 | return $this->memcache->get($this->prefix . $key) ?: ''; 41 | } 42 | 43 | public function clear(): bool 44 | { 45 | return $this->memcache->flush(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php: -------------------------------------------------------------------------------- 1 | memcache->set($this->prefix . $key, $value, $ttl); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Cache/NoCache.php: -------------------------------------------------------------------------------- 1 | prefix = $prefix; 15 | if ($config == '') { 16 | $config = '127.0.0.1'; 17 | } 18 | $params = explode(':', $config, 6); 19 | if (isset($params[3])) { 20 | $params[3] = null; 21 | } 22 | $this->redis = new \Redis(); 23 | call_user_func_array(array($this->redis, 'pconnect'), $params); 24 | } 25 | 26 | public function set(string $key, string $value, int $ttl = 0): bool 27 | { 28 | return $this->redis->set($this->prefix . $key, $value, $ttl); 29 | } 30 | 31 | public function get(string $key): string 32 | { 33 | return $this->redis->get($this->prefix . $key) ?: ''; 34 | } 35 | 36 | public function clear(): bool 37 | { 38 | return $this->redis->flushDb(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Cache/TempFileCache.php: -------------------------------------------------------------------------------- 1 | segments = []; 17 | $s = DIRECTORY_SEPARATOR; 18 | $ps = PATH_SEPARATOR; 19 | if ($config == '') { 20 | $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX; 21 | } elseif (strpos($config, $ps) === false) { 22 | $this->path = $config; 23 | } else { 24 | list($path, $segments) = explode($ps, $config); 25 | $this->path = $path; 26 | $this->segments = explode(',', $segments); 27 | } 28 | if (file_exists($this->path) && is_dir($this->path)) { 29 | $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false); 30 | } 31 | } 32 | 33 | private function getFileName(string $key): string 34 | { 35 | $s = DIRECTORY_SEPARATOR; 36 | $md5 = md5($key); 37 | $filename = rtrim($this->path, $s) . $s; 38 | $i = 0; 39 | foreach ($this->segments as $segment) { 40 | $filename .= substr($md5, $i, $segment) . $s; 41 | $i += $segment; 42 | } 43 | $filename .= substr($md5, $i); 44 | return $filename; 45 | } 46 | 47 | public function set(string $key, string $value, int $ttl = 0): bool 48 | { 49 | $filename = $this->getFileName($key); 50 | $dirname = dirname($filename); 51 | if (!file_exists($dirname)) { 52 | if (!mkdir($dirname, 0755, true)) { 53 | return false; 54 | } 55 | } 56 | $string = $ttl . '|' . $value; 57 | return $this->filePutContents($filename, $string) !== false; 58 | } 59 | 60 | private function filePutContents($filename, $string) 61 | { 62 | return file_put_contents($filename, $string, LOCK_EX); 63 | } 64 | 65 | private function fileGetContents($filename) 66 | { 67 | $file = fopen($filename, 'rb'); 68 | if ($file === false) { 69 | return false; 70 | } 71 | $lock = flock($file, LOCK_SH); 72 | if (!$lock) { 73 | fclose($file); 74 | return false; 75 | } 76 | $string = ''; 77 | while (!feof($file)) { 78 | $string .= fread($file, 8192); 79 | } 80 | flock($file, LOCK_UN); 81 | fclose($file); 82 | return $string; 83 | } 84 | 85 | private function getString($filename): string 86 | { 87 | $data = $this->fileGetContents($filename); 88 | if ($data === false) { 89 | return ''; 90 | } 91 | if (strpos($data, '|') === false) { 92 | return ''; 93 | } 94 | list($ttl, $string) = explode('|', $data, 2); 95 | if ($ttl > 0 && time() - filemtime($filename) > $ttl) { 96 | return ''; 97 | } 98 | return $string; 99 | } 100 | 101 | public function get(string $key): string 102 | { 103 | $filename = $this->getFileName($key); 104 | if (!file_exists($filename)) { 105 | return ''; 106 | } 107 | $string = $this->getString($filename); 108 | if ($string == null) { 109 | return ''; 110 | } 111 | return $string; 112 | } 113 | 114 | private function clean(string $path, array $segments, int $len, bool $all) /*: void*/ 115 | { 116 | $entries = scandir($path); 117 | foreach ($entries as $entry) { 118 | if ($entry === '.' || $entry === '..') { 119 | continue; 120 | } 121 | $filename = $path . DIRECTORY_SEPARATOR . $entry; 122 | if (count($segments) == 0) { 123 | if (strlen($entry) != $len) { 124 | continue; 125 | } 126 | if (file_exists($filename) && is_file($filename)) { 127 | if ($all || $this->getString($filename) == null) { 128 | @unlink($filename); 129 | } 130 | } 131 | } else { 132 | if (strlen($entry) != $segments[0]) { 133 | continue; 134 | } 135 | if (file_exists($filename) && is_dir($filename)) { 136 | $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all); 137 | @rmdir($filename); 138 | } 139 | } 140 | } 141 | } 142 | 143 | public function clear(): bool 144 | { 145 | if (!file_exists($this->path) || !is_dir($this->path)) { 146 | return false; 147 | } 148 | $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true); 149 | return true; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Column/DefinitionService.php: -------------------------------------------------------------------------------- 1 | db = $db; 17 | $this->reflection = $reflection; 18 | } 19 | 20 | public function updateTable(ReflectedTable $table, /* object */ $changes): bool 21 | { 22 | $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes)); 23 | if ($table->getRealName() != $newTable->getRealName()) { 24 | if (!$this->db->definition()->renameTable($table->getRealName(), $newTable->getRealName())) { 25 | return false; 26 | } 27 | } 28 | return true; 29 | } 30 | 31 | public function updateColumn(ReflectedTable $table, ReflectedColumn $column, /* object */ $changes): bool 32 | { 33 | // remove constraints on other column 34 | $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes)); 35 | if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) { 36 | $oldColumn = $table->getPk(); 37 | if ($oldColumn->getRealName() != $column->getRealName()) { 38 | $oldColumn->setPk(false); 39 | if (!$this->db->definition()->removeColumnPrimaryKey($table->getRealName(), $oldColumn->getRealName(), $oldColumn)) { 40 | return false; 41 | } 42 | } 43 | } 44 | 45 | // remove constraints 46 | $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false])); 47 | if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) { 48 | if (!$this->db->definition()->removeColumnPrimaryKey($table->getRealName(), $column->getRealName(), $newColumn)) { 49 | return false; 50 | } 51 | } 52 | if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) { 53 | if (!$this->db->definition()->removeColumnForeignKey($table->getRealName(), $column->getRealName(), $newColumn)) { 54 | return false; 55 | } 56 | } 57 | 58 | // name and type 59 | $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes)); 60 | $newColumn->setPk(false); 61 | $newColumn->setFk(''); 62 | if ($newColumn->getRealName() != $column->getRealName()) { 63 | if (!$this->db->definition()->renameColumn($table->getRealName(), $column->getRealName(), $newColumn)) { 64 | return false; 65 | } 66 | } 67 | if ( 68 | $newColumn->getType() != $column->getType() || 69 | $newColumn->getLength() != $column->getLength() || 70 | $newColumn->getPrecision() != $column->getPrecision() || 71 | $newColumn->getScale() != $column->getScale() 72 | ) { 73 | if (!$this->db->definition()->retypeColumn($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 74 | return false; 75 | } 76 | } 77 | if ($newColumn->getNullable() != $column->getNullable()) { 78 | if (!$this->db->definition()->setColumnNullable($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 79 | return false; 80 | } 81 | } 82 | 83 | // add constraints 84 | $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes)); 85 | if ($newColumn->getFk()) { 86 | if (!$this->db->definition()->addColumnForeignKey($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 87 | return false; 88 | } 89 | } 90 | if ($newColumn->getPk()) { 91 | if (!$this->db->definition()->addColumnPrimaryKey($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 92 | return false; 93 | } 94 | } 95 | return true; 96 | } 97 | 98 | public function addTable(/* object */$definition) 99 | { 100 | $newTable = ReflectedTable::fromJson($definition); 101 | if (!$this->db->definition()->addTable($newTable)) { 102 | return false; 103 | } 104 | return true; 105 | } 106 | 107 | public function addColumn(ReflectedTable $table, /* object */ $definition) 108 | { 109 | $newColumn = ReflectedColumn::fromJson($definition); 110 | if (!$this->db->definition()->addColumn($table->getRealName(), $newColumn)) { 111 | return false; 112 | } 113 | if ($newColumn->getFk()) { 114 | if (!$this->db->definition()->addColumnForeignKey($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 115 | return false; 116 | } 117 | } 118 | if ($newColumn->getPk()) { 119 | if (!$this->db->definition()->addColumnPrimaryKey($table->getRealName(), $newColumn->getRealName(), $newColumn)) { 120 | return false; 121 | } 122 | } 123 | return true; 124 | } 125 | 126 | public function removeTable(ReflectedTable $table) 127 | { 128 | if (!$this->db->definition()->removeTable($table->getRealName())) { 129 | return false; 130 | } 131 | return true; 132 | } 133 | 134 | public function removeColumn(ReflectedTable $table, ReflectedColumn $column) 135 | { 136 | if ($column->getPk()) { 137 | $column->setPk(false); 138 | if (!$this->db->definition()->removeColumnPrimaryKey($table->getRealName(), $column->getRealName(), $column)) { 139 | return false; 140 | } 141 | } 142 | if ($column->getFk()) { 143 | $column->setFk(""); 144 | if (!$this->db->definition()->removeColumnForeignKey($table->getRealName(), $column->getRealName(), $column)) { 145 | return false; 146 | } 147 | } 148 | if (!$this->db->definition()->removeColumn($table->getRealName(), $column->getRealName())) { 149 | return false; 150 | } 151 | return true; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->realName = $realName; 27 | $this->type = $type; 28 | $this->length = $length; 29 | $this->precision = $precision; 30 | $this->scale = $scale; 31 | $this->nullable = $nullable; 32 | $this->pk = $pk; 33 | $this->fk = $fk; 34 | $this->sanitize(); 35 | } 36 | 37 | private static function parseColumnType(string $columnType, int &$length, int &$precision, int &$scale) /*: void*/ 38 | { 39 | if (!$columnType) { 40 | return; 41 | } 42 | $pos = strpos($columnType, '('); 43 | if ($pos) { 44 | $dataSize = rtrim(substr($columnType, $pos + 1), ')'); 45 | if ($length) { 46 | $length = (int) $dataSize; 47 | } else { 48 | $pos = strpos($dataSize, ','); 49 | if ($pos) { 50 | $precision = (int) substr($dataSize, 0, $pos); 51 | $scale = (int) substr($dataSize, $pos + 1); 52 | } else { 53 | $precision = (int) $dataSize; 54 | $scale = 0; 55 | } 56 | } 57 | } 58 | } 59 | 60 | private static function getDataSize(int $length, int $precision, int $scale): string 61 | { 62 | $dataSize = ''; 63 | if ($length) { 64 | $dataSize = $length; 65 | } elseif ($precision) { 66 | if ($scale) { 67 | $dataSize = $precision . ',' . $scale; 68 | } else { 69 | $dataSize = $precision; 70 | } 71 | } 72 | return $dataSize; 73 | } 74 | 75 | public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn 76 | { 77 | $name = $columnResult['COLUMN_NAME']; 78 | $realName = $columnResult['COLUMN_REAL_NAME']; 79 | $dataType = $columnResult['DATA_TYPE']; 80 | $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH']; 81 | $precision = (int) $columnResult['NUMERIC_PRECISION']; 82 | $scale = (int) $columnResult['NUMERIC_SCALE']; 83 | $columnType = $columnResult['COLUMN_TYPE']; 84 | self::parseColumnType($columnType, $length, $precision, $scale); 85 | $dataSize = self::getDataSize($length, $precision, $scale); 86 | $type = $reflection->toJdbcType($dataType, $dataSize); 87 | $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']); 88 | $pk = false; 89 | $fk = ''; 90 | return new ReflectedColumn($name, $realName, $type, $length, $precision, $scale, $nullable, $pk, $fk); 91 | } 92 | 93 | public static function fromJson( /* object */$json): ReflectedColumn 94 | { 95 | $name = $json->alias ?? $json->name; 96 | $realName = $json->name; 97 | $type = $json->type; 98 | $length = isset($json->length) ? (int) $json->length : 0; 99 | $precision = isset($json->precision) ? (int) $json->precision : 0; 100 | $scale = isset($json->scale) ? (int) $json->scale : 0; 101 | $nullable = isset($json->nullable) ? (bool) $json->nullable : false; 102 | $pk = isset($json->pk) ? (bool) $json->pk : false; 103 | $fk = isset($json->fk) ? $json->fk : ''; 104 | return new ReflectedColumn($name, $realName, $type, $length, $precision, $scale, $nullable, $pk, $fk); 105 | } 106 | 107 | private function sanitize() 108 | { 109 | $this->length = $this->hasLength() ? $this->getLength() : 0; 110 | $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0; 111 | $this->scale = $this->hasScale() ? $this->getScale() : 0; 112 | } 113 | 114 | public function getName(): string 115 | { 116 | return $this->name; 117 | } 118 | 119 | public function getRealName(): string 120 | { 121 | return $this->realName; 122 | } 123 | 124 | public function getNullable(): bool 125 | { 126 | return $this->nullable; 127 | } 128 | 129 | public function getType(): string 130 | { 131 | return $this->type; 132 | } 133 | 134 | public function getLength(): int 135 | { 136 | return $this->length ?: self::DEFAULT_LENGTH; 137 | } 138 | 139 | public function getPrecision(): int 140 | { 141 | return $this->precision ?: self::DEFAULT_PRECISION; 142 | } 143 | 144 | public function getScale(): int 145 | { 146 | return $this->scale ?: self::DEFAULT_SCALE; 147 | } 148 | 149 | public function hasLength(): bool 150 | { 151 | return in_array($this->type, ['varchar', 'varbinary']); 152 | } 153 | 154 | public function hasPrecision(): bool 155 | { 156 | return $this->type == 'decimal'; 157 | } 158 | 159 | public function hasScale(): bool 160 | { 161 | return $this->type == 'decimal'; 162 | } 163 | 164 | public function isBinary(): bool 165 | { 166 | return in_array($this->type, ['blob', 'varbinary']); 167 | } 168 | 169 | public function isBoolean(): bool 170 | { 171 | return $this->type == 'boolean'; 172 | } 173 | 174 | public function isGeometry(): bool 175 | { 176 | return $this->type == 'geometry'; 177 | } 178 | 179 | public function isInteger(): bool 180 | { 181 | return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']); 182 | } 183 | 184 | public function isText(): bool 185 | { 186 | return in_array($this->type, ['varchar', 'clob']); 187 | } 188 | 189 | public function setPk($value) /*: void*/ 190 | { 191 | $this->pk = $value; 192 | } 193 | 194 | public function getPk(): bool 195 | { 196 | return $this->pk; 197 | } 198 | 199 | public function setFk($value) /*: void*/ 200 | { 201 | $this->fk = $value; 202 | } 203 | 204 | public function getFk(): string 205 | { 206 | return $this->fk; 207 | } 208 | 209 | public function serialize() 210 | { 211 | $json = [ 212 | 'name' => $this->realName, 213 | 'alias' => $this->name != $this->realName ? $this->name : null, 214 | 'type' => $this->type, 215 | 'length' => $this->length, 216 | 'precision' => $this->precision, 217 | 'scale' => $this->scale, 218 | 'nullable' => $this->nullable, 219 | 'pk' => $this->pk, 220 | 'fk' => $this->fk, 221 | ]; 222 | return array_filter($json); 223 | } 224 | 225 | #[\ReturnTypeWillChange] 226 | public function jsonSerialize() 227 | { 228 | return $this->serialize(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php: -------------------------------------------------------------------------------- 1 | tableTypes = $tableTypes; 15 | $this->tableRealNames = $tableRealNames; 16 | } 17 | 18 | public static function fromReflection(GenericReflection $reflection): ReflectedDatabase 19 | { 20 | $tableTypes = []; 21 | $tableRealNames = []; 22 | foreach ($reflection->getTables() as $table) { 23 | $tableName = $table['TABLE_NAME']; 24 | if (in_array($tableName, $reflection->getIgnoredTables())) { 25 | continue; 26 | } 27 | $tableTypes[$tableName] = $table['TABLE_TYPE']; 28 | $tableRealNames[$tableName] = $table['TABLE_REAL_NAME']; 29 | } 30 | return new ReflectedDatabase($tableTypes, $tableRealNames); 31 | } 32 | 33 | public static function fromJson( /* object */$json): ReflectedDatabase 34 | { 35 | $tableTypes = (array) $json->types; 36 | $tableRealNames = (array) $json->realNames; 37 | return new ReflectedDatabase($tableTypes, $tableRealNames); 38 | } 39 | 40 | public function hasTable(string $tableName): bool 41 | { 42 | return isset($this->tableTypes[$tableName]); 43 | } 44 | 45 | public function getType(string $tableName): string 46 | { 47 | return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : ''; 48 | } 49 | 50 | public function getRealName(string $tableName): string 51 | { 52 | return isset($this->tableRealNames[$tableName]) ? $this->tableRealNames[$tableName] : ''; 53 | } 54 | 55 | public function getTableNames(): array 56 | { 57 | return array_keys($this->tableTypes); 58 | } 59 | 60 | public function removeTable(string $tableName): bool 61 | { 62 | if (!isset($this->tableTypes[$tableName])) { 63 | return false; 64 | } 65 | unset($this->tableTypes[$tableName]); 66 | unset($this->tableRealNames[$tableName]); 67 | return true; 68 | } 69 | 70 | public function serialize() 71 | { 72 | return [ 73 | 'types' => $this->tableTypes, 74 | 'realNames' => $this->tableRealNames, 75 | ]; 76 | } 77 | 78 | #[\ReturnTypeWillChange] 79 | public function jsonSerialize() 80 | { 81 | return $this->serialize(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php: -------------------------------------------------------------------------------- 1 | name = $name; 19 | $this->realName = $realName; 20 | $this->type = $type; 21 | // set columns 22 | $this->columns = []; 23 | foreach ($columns as $column) { 24 | $columnName = $column->getName(); 25 | $this->columns[$columnName] = $column; 26 | } 27 | // set primary key 28 | $this->pk = null; 29 | foreach ($columns as $column) { 30 | if ($column->getPk() == true) { 31 | $this->pk = $column; 32 | } 33 | } 34 | // set foreign keys 35 | $this->fks = []; 36 | foreach ($columns as $column) { 37 | $columnName = $column->getName(); 38 | $referencedTableName = $column->getFk(); 39 | if ($referencedTableName != '') { 40 | $this->fks[$columnName] = $referencedTableName; 41 | } 42 | } 43 | } 44 | 45 | public static function fromReflection(GenericReflection $reflection, string $name, string $realName, string $type): ReflectedTable 46 | { 47 | // set columns 48 | $columns = []; 49 | foreach ($reflection->getTableColumns($name, $type) as $tableColumn) { 50 | $column = ReflectedColumn::fromReflection($reflection, $tableColumn); 51 | $columns[$column->getName()] = $column; 52 | } 53 | // set primary key 54 | $columnName = false; 55 | if ($type == 'view') { 56 | $columnName = 'id'; 57 | } else { 58 | $columnNames = $reflection->getTablePrimaryKeys($name); 59 | if (count($columnNames) == 1) { 60 | $columnName = $columnNames[0]; 61 | } 62 | } 63 | if ($columnName && isset($columns[$columnName])) { 64 | $pk = $columns[$columnName]; 65 | $pk->setPk(true); 66 | } 67 | // set foreign keys 68 | if ($type == 'view') { 69 | $tables = $reflection->getTables(); 70 | foreach ($columns as $columnName => $column) { 71 | if (substr($columnName, -3) == '_id') { 72 | foreach ($tables as $table) { 73 | $tableName = $table['TABLE_NAME']; 74 | $suffix = $tableName . '_id'; 75 | if (substr($columnName, -1 * strlen($suffix)) == $suffix) { 76 | $column->setFk($tableName); 77 | } 78 | } 79 | } 80 | } 81 | } else { 82 | $fks = $reflection->getTableForeignKeys($name); 83 | foreach ($fks as $columnName => $table) { 84 | $columns[$columnName]->setFk($table); 85 | } 86 | } 87 | return new ReflectedTable($name, $realName, $type, array_values($columns)); 88 | } 89 | 90 | public static function fromJson( /* object */$json): ReflectedTable 91 | { 92 | $name = $json->alias??$json->name; 93 | $realName = $json->name; 94 | $type = isset($json->type) ? $json->type : 'table'; 95 | $columns = []; 96 | if (isset($json->columns) && is_array($json->columns)) { 97 | foreach ($json->columns as $column) { 98 | $columns[] = ReflectedColumn::fromJson($column); 99 | } 100 | } 101 | return new ReflectedTable($name, $realName, $type, $columns); 102 | } 103 | 104 | public function hasColumn(string $columnName): bool 105 | { 106 | return isset($this->columns[$columnName]); 107 | } 108 | 109 | public function hasPk(): bool 110 | { 111 | return $this->pk != null; 112 | } 113 | 114 | public function getPk() /*: ?ReflectedColumn */ 115 | { 116 | return $this->pk; 117 | } 118 | 119 | public function getName(): string 120 | { 121 | return $this->name; 122 | } 123 | 124 | public function getRealName(): string 125 | { 126 | return $this->realName; 127 | } 128 | 129 | public function getType(): string 130 | { 131 | return $this->type; 132 | } 133 | 134 | public function getColumnNames(): array 135 | { 136 | return array_keys($this->columns); 137 | } 138 | 139 | public function getColumn($columnName): ReflectedColumn 140 | { 141 | return $this->columns[$columnName]; 142 | } 143 | 144 | public function getFksTo(string $tableName): array 145 | { 146 | $columns = array(); 147 | foreach ($this->fks as $columnName => $referencedTableName) { 148 | if ($tableName == $referencedTableName && !is_null($this->columns[$columnName])) { 149 | $columns[] = $this->columns[$columnName]; 150 | } 151 | } 152 | return $columns; 153 | } 154 | 155 | public function removeColumn(string $columnName): bool 156 | { 157 | if (!isset($this->columns[$columnName])) { 158 | return false; 159 | } 160 | unset($this->columns[$columnName]); 161 | return true; 162 | } 163 | 164 | public function serialize() 165 | { 166 | $json = [ 167 | 'name' => $this->realName, 168 | 'alias' => $this->name!=$this->realName?$this->name:null, 169 | 'type' => $this->type, 170 | 'columns' => array_values($this->columns), 171 | ]; 172 | return array_filter($json); 173 | } 174 | 175 | #[\ReturnTypeWillChange] 176 | public function jsonSerialize() 177 | { 178 | return $this->serialize(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Column/ReflectionService.php: -------------------------------------------------------------------------------- 1 | db = $db; 21 | $this->cache = $cache; 22 | $this->ttl = $ttl; 23 | $this->database = null; 24 | $this->tables = []; 25 | } 26 | 27 | private function database(): ReflectedDatabase 28 | { 29 | if ($this->database) { 30 | return $this->database; 31 | } 32 | $this->database = $this->loadDatabase(true); 33 | return $this->database; 34 | } 35 | 36 | private function loadDatabase(bool $useCache): ReflectedDatabase 37 | { 38 | $key = sprintf('%s-ReflectedDatabase', $this->db->getCacheKey()); 39 | $data = $useCache ? $this->cache->get($key) : ''; 40 | if ($data != '') { 41 | $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data))); 42 | } else { 43 | $database = ReflectedDatabase::fromReflection($this->db->reflection()); 44 | $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE)); 45 | $this->cache->set($key, $data, $this->ttl); 46 | } 47 | return $database; 48 | } 49 | 50 | private function loadTable(string $tableName, bool $useCache): ReflectedTable 51 | { 52 | $key = sprintf('%s-ReflectedTable(%s)', $this->db->getCacheKey(), $tableName); 53 | $data = $useCache ? $this->cache->get($key) : ''; 54 | if ($data != '') { 55 | $table = ReflectedTable::fromJson(json_decode(gzuncompress($data))); 56 | } else { 57 | $tableType = $this->database()->getType($tableName); 58 | $tableRealName = $this->database()->getRealName($tableName); 59 | $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableRealName, $tableType); 60 | $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE)); 61 | $this->cache->set($key, $data, $this->ttl); 62 | } 63 | return $table; 64 | } 65 | 66 | public function refreshTables() 67 | { 68 | $this->database = $this->loadDatabase(false); 69 | } 70 | 71 | public function refreshTable(string $tableName) 72 | { 73 | $this->tables[$tableName] = $this->loadTable($tableName, false); 74 | } 75 | 76 | public function hasTable(string $tableName): bool 77 | { 78 | return $this->database()->hasTable($tableName); 79 | } 80 | 81 | public function getType(string $tableName): string 82 | { 83 | return $this->database()->getType($tableName); 84 | } 85 | 86 | public function getTable(string $tableName): ReflectedTable 87 | { 88 | if (!isset($this->tables[$tableName])) { 89 | $this->tables[$tableName] = $this->loadTable($tableName, true); 90 | } 91 | return $this->tables[$tableName]; 92 | } 93 | 94 | public function getTableNames(): array 95 | { 96 | return $this->database()->getTableNames(); 97 | } 98 | 99 | public function removeTable(string $tableName): bool 100 | { 101 | unset($this->tables[$tableName]); 102 | return $this->database()->removeTable($tableName); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Config/Base/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | null, 11 | 'address' => null, 12 | 'port' => null, 13 | 'username' => '', 14 | 'password' => '', 15 | 'database' => '', 16 | 'command' => '', 17 | 'tables' => 'all', 18 | 'mapping' => '', 19 | 'middlewares' => 'cors', 20 | 'controllers' => 'records,geojson,openapi,status', 21 | 'customControllers' => '', 22 | 'customOpenApiBuilders' => '', 23 | 'cacheType' => 'TempFile', 24 | 'cachePath' => '', 25 | 'cacheTime' => 10, 26 | 'jsonOptions' => JSON_UNESCAPED_UNICODE, 27 | 'debug' => false, 28 | 'basePath' => '', 29 | 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}', 30 | 'geometrySrid' => 4326, 31 | ]; 32 | 33 | public function getUID(): string 34 | { 35 | return md5(json_encode($this->values)); 36 | } 37 | 38 | private function getDefaultDriver(array $values): string 39 | { 40 | if (isset($values['driver'])) { 41 | return $values['driver']; 42 | } 43 | return 'mysql'; 44 | } 45 | 46 | private function getDefaultPort(string $driver): int 47 | { 48 | switch ($driver) { 49 | case 'mysql': 50 | return 3306; 51 | case 'pgsql': 52 | return 5432; 53 | case 'sqlsrv': 54 | return 1433; 55 | case 'sqlite': 56 | return 0; 57 | } 58 | } 59 | 60 | private function getDefaultAddress(string $driver): string 61 | { 62 | switch ($driver) { 63 | case 'mysql': 64 | return 'localhost'; 65 | case 'pgsql': 66 | return 'localhost'; 67 | case 'sqlsrv': 68 | return 'localhost'; 69 | case 'sqlite': 70 | return 'data.db'; 71 | } 72 | } 73 | 74 | private function getDriverDefaults(string $driver): array 75 | { 76 | return [ 77 | 'driver' => $driver, 78 | 'address' => $this->getDefaultAddress($driver), 79 | 'port' => $this->getDefaultPort($driver), 80 | ]; 81 | } 82 | 83 | private function getEnvironmentVariableName(string $key): string 84 | { 85 | $prefix = "PHP_CRUD_API_"; 86 | $suffix = strtoupper(preg_replace('/(?values[$key] ?? $default; 94 | } 95 | $variableName = $this->getEnvironmentVariableName($key); 96 | return getenv($variableName, true) ?: ($this->values[$key] ?? $default); 97 | } 98 | 99 | public function __construct(array $values) 100 | { 101 | $defaults = array_merge($this->values, $this->getDriverDefaults($this->getDefaultDriver($values))); 102 | foreach ($defaults as $key => $default) { 103 | $this->values[$key] = $values[$key] ?? $default; 104 | $this->values[$key] = $this->getProperty($key); 105 | } 106 | $this->values['middlewares'] = array_map('trim', explode(',', $this->values['middlewares'])); 107 | foreach ($values as $key => $value) { 108 | if (strpos($key, '.') === false) { 109 | if (!isset($defaults[$key])) { 110 | throw new \Exception("Config has invalid key '$key'"); 111 | } 112 | } else { 113 | $middleware = substr($key, 0, strpos($key, '.')); 114 | if (!in_array($middleware, $this->values['middlewares'])) { 115 | throw new \Exception("Config has invalid middleware key '$key'"); 116 | } else { 117 | $this->values[$key] = $value; 118 | } 119 | } 120 | } 121 | } 122 | 123 | public function getDriver(): string 124 | { 125 | return $this->values['driver']; 126 | } 127 | 128 | public function getAddress(): string 129 | { 130 | return $this->values['address']; 131 | } 132 | 133 | public function getPort(): int 134 | { 135 | return $this->values['port']; 136 | } 137 | 138 | public function getUsername(): string 139 | { 140 | return $this->values['username']; 141 | } 142 | 143 | public function getPassword(): string 144 | { 145 | return $this->values['password']; 146 | } 147 | 148 | public function getDatabase(): string 149 | { 150 | return $this->values['database']; 151 | } 152 | 153 | public function getCommand(): string 154 | { 155 | return $this->values['command']; 156 | } 157 | 158 | 159 | public function getTables(): array 160 | { 161 | return array_filter(array_map('trim', explode(',', $this->values['tables']))); 162 | } 163 | 164 | public function getMapping(): array 165 | { 166 | $mapping = array_map(function ($v) { 167 | return explode('=', $v); 168 | }, array_filter(array_map('trim', explode(',', $this->values['mapping'])))); 169 | return array_combine(array_column($mapping, 0), array_column($mapping, 1)); 170 | } 171 | 172 | public function getMiddlewares(): array 173 | { 174 | return $this->values['middlewares']; 175 | } 176 | 177 | public function getControllers(): array 178 | { 179 | return array_filter(array_map('trim', explode(',', $this->values['controllers']))); 180 | } 181 | 182 | public function getCustomControllers(): array 183 | { 184 | return array_filter(array_map('trim', explode(',', $this->values['customControllers']))); 185 | } 186 | 187 | public function getCustomOpenApiBuilders(): array 188 | { 189 | return array_filter(array_map('trim', explode(',', $this->values['customOpenApiBuilders']))); 190 | } 191 | 192 | public function getCacheType(): string 193 | { 194 | return $this->values['cacheType']; 195 | } 196 | 197 | public function getCachePath(): string 198 | { 199 | return $this->values['cachePath']; 200 | } 201 | 202 | public function getCacheTime(): int 203 | { 204 | return $this->values['cacheTime']; 205 | } 206 | 207 | public function getJsonOptions(): int 208 | { 209 | return $this->values['jsonOptions']; 210 | } 211 | 212 | public function getDebug(): bool 213 | { 214 | return $this->values['debug']; 215 | } 216 | 217 | public function getBasePath(): string 218 | { 219 | return $this->values['basePath']; 220 | } 221 | 222 | public function getOpenApiBase(): array 223 | { 224 | return json_decode($this->values['openApiBase'], true); 225 | } 226 | 227 | public function getGeometrySrid(): int 228 | { 229 | return $this->values['geometrySrid']; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Controller/CacheController.php: -------------------------------------------------------------------------------- 1 | register('GET', '/cache/clear', array($this, 'clear')); 18 | $this->cache = $cache; 19 | $this->responder = $responder; 20 | } 21 | 22 | public function clear(ServerRequestInterface $request): ResponseInterface 23 | { 24 | return $this->responder->success($this->cache->clear()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Controller/GeoJsonController.php: -------------------------------------------------------------------------------- 1 | register('GET', '/geojson/*', array($this, '_list')); 20 | $router->register('GET', '/geojson/*/*', array($this, 'read')); 21 | $this->service = $service; 22 | $this->responder = $responder; 23 | } 24 | 25 | public function _list(ServerRequestInterface $request): ResponseInterface 26 | { 27 | $table = RequestUtils::getPathSegment($request, 2); 28 | $params = RequestUtils::getParams($request); 29 | if (!$this->service->hasTable($table)) { 30 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 31 | } 32 | return $this->responder->success($this->service->_list($table, $params)); 33 | } 34 | 35 | public function read(ServerRequestInterface $request): ResponseInterface 36 | { 37 | $table = RequestUtils::getPathSegment($request, 2); 38 | if (!$this->service->hasTable($table)) { 39 | return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table); 40 | } 41 | if ($this->service->getType($table) != 'table') { 42 | return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__); 43 | } 44 | $id = RequestUtils::getPathSegment($request, 3); 45 | $params = RequestUtils::getParams($request); 46 | if (strpos($id, ',') !== false) { 47 | $ids = explode(',', $id); 48 | $result = (object) array('type' => 'FeatureCollection', 'features' => array()); 49 | for ($i = 0; $i < count($ids); $i++) { 50 | array_push($result->features, $this->service->read($table, $ids[$i], $params)); 51 | } 52 | return $this->responder->success($result); 53 | } else { 54 | $response = $this->service->read($table, $id, $params); 55 | if ($response === null) { 56 | return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id); 57 | } 58 | return $this->responder->success($response); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Controller/JsonResponder.php: -------------------------------------------------------------------------------- 1 | jsonOptions = $jsonOptions; 19 | $this->debug = $debug; 20 | } 21 | 22 | public function error(int $error, string $argument, $details = null): ResponseInterface 23 | { 24 | $document = new ErrorDocument(new ErrorCode($error), $argument, $details); 25 | return ResponseFactory::fromObject($document->getStatus(), $document, $this->jsonOptions); 26 | } 27 | 28 | public function success($result): ResponseInterface 29 | { 30 | return ResponseFactory::fromObject(ResponseFactory::OK, $result, $this->jsonOptions); 31 | } 32 | 33 | public function exception($exception): ResponseInterface 34 | { 35 | $document = ErrorDocument::fromException($exception, $this->debug); 36 | $response = ResponseFactory::fromObject($document->getStatus(), $document, $this->jsonOptions); 37 | if ($this->debug) { 38 | $response = ResponseUtils::addExceptionHeaders($response, $exception); 39 | } 40 | return $response; 41 | } 42 | 43 | public function multi($results): ResponseInterface 44 | { 45 | $documents = array(); 46 | $errors = array(); 47 | $success = true; 48 | foreach ($results as $i => $result) { 49 | if ($result instanceof \Throwable) { 50 | $documents[$i] = null; 51 | $errors[$i] = ErrorDocument::fromException($result, $this->debug); 52 | $success = false; 53 | } else { 54 | $documents[$i] = $result; 55 | $errors[$i] = new ErrorDocument(new ErrorCode(0), '', null); 56 | } 57 | } 58 | $status = $success ? ResponseFactory::OK : ResponseFactory::FAILED_DEPENDENCY; 59 | $document = $success ? $documents : $errors; 60 | $response = ResponseFactory::fromObject($status, $document, $this->jsonOptions); 61 | foreach ($results as $i => $result) { 62 | if ($result instanceof \Throwable) { 63 | if ($this->debug) { 64 | $response = ResponseUtils::addExceptionHeaders($response, $result); 65 | } 66 | } 67 | } 68 | return $response; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Controller/OpenApiController.php: -------------------------------------------------------------------------------- 1 | register('GET', '/openapi', array($this, 'openapi')); 18 | $this->openApi = $openApi; 19 | $this->responder = $responder; 20 | } 21 | 22 | public function openapi(ServerRequestInterface $request): ResponseInterface 23 | { 24 | return $this->responder->success($this->openApi->get($request)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Controller/Responder.php: -------------------------------------------------------------------------------- 1 | register('GET', '/status/ping', array($this, 'ping')); 20 | $this->db = $db; 21 | $this->cache = $cache; 22 | $this->responder = $responder; 23 | } 24 | 25 | public function ping(ServerRequestInterface $request): ResponseInterface 26 | { 27 | $result = [ 28 | 'db' => $this->db->ping(), 29 | 'cache' => $this->cache->ping(), 30 | ]; 31 | return $this->responder->success($result); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Database/ColumnConverter.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 15 | $this->geometrySrid = $geometrySrid; 16 | } 17 | 18 | public function convertColumnValue(ReflectedColumn $column): string 19 | { 20 | if ($column->isBoolean()) { 21 | switch ($this->driver) { 22 | case 'mysql': 23 | return "IFNULL(IF(?,TRUE,FALSE),NULL)"; 24 | case 'pgsql': 25 | return "?"; 26 | case 'sqlsrv': 27 | return "?"; 28 | } 29 | } 30 | if ($column->isBinary()) { 31 | switch ($this->driver) { 32 | case 'mysql': 33 | return "FROM_BASE64(?)"; 34 | case 'pgsql': 35 | return "decode(?, 'base64')"; 36 | case 'sqlsrv': 37 | return "CONVERT(XML, ?).value('.','varbinary(max)')"; 38 | } 39 | } 40 | if ($column->isGeometry()) { 41 | $srid = $this->geometrySrid; 42 | switch ($this->driver) { 43 | case 'mysql': 44 | case 'pgsql': 45 | return "ST_GeomFromText(?,$srid)"; 46 | case 'sqlsrv': 47 | return "geometry::STGeomFromText(?,$srid)"; 48 | } 49 | } 50 | return '?'; 51 | } 52 | 53 | public function convertColumnName(ReflectedColumn $column, $value): string 54 | { 55 | if ($column->isBinary()) { 56 | switch ($this->driver) { 57 | case 'mysql': 58 | return "TO_BASE64($value) as $value"; 59 | case 'pgsql': 60 | return "encode($value::bytea, 'base64') as $value"; 61 | case 'sqlsrv': 62 | return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value"; 63 | } 64 | } 65 | if ($column->isGeometry()) { 66 | switch ($this->driver) { 67 | case 'mysql': 68 | case 'pgsql': 69 | return "ST_AsText($value) as $value"; 70 | case 'sqlsrv': 71 | return "REPLACE($value.STAsText(),' (','(') as $value"; 72 | } 73 | } 74 | return $value; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 16 | $this->converter = new ColumnConverter($driver, $geometrySrid); 17 | } 18 | 19 | public function getOffsetLimit(int $offset, int $limit): string 20 | { 21 | if ($limit < 0 || $offset < 0) { 22 | return ''; 23 | } 24 | switch ($this->driver) { 25 | case 'mysql': 26 | return " LIMIT $offset, $limit"; 27 | case 'pgsql': 28 | return " LIMIT $limit OFFSET $offset"; 29 | case 'sqlsrv': 30 | return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY"; 31 | case 'sqlite': 32 | return " LIMIT $limit OFFSET $offset"; 33 | } 34 | } 35 | 36 | private function quoteColumnName(ReflectedColumn $column): string 37 | { 38 | return '"' . $column->getRealName() . '"'; 39 | } 40 | 41 | public function getOrderBy(ReflectedTable $table, array $columnOrdering): string 42 | { 43 | if (count($columnOrdering) == 0) { 44 | return ''; 45 | } 46 | $results = array(); 47 | foreach ($columnOrdering as $i => list($columnName, $ordering)) { 48 | $column = $table->getColumn($columnName); 49 | $quotedColumnName = $this->quoteColumnName($column); 50 | $results[] = $quotedColumnName . ' ' . $ordering; 51 | } 52 | return ' ORDER BY ' . implode(',', $results); 53 | } 54 | 55 | public function getSelect(ReflectedTable $table, array $columnNames): string 56 | { 57 | $results = array(); 58 | foreach ($columnNames as $columnName) { 59 | $column = $table->getColumn($columnName); 60 | $quotedColumnName = $this->quoteColumnName($column); 61 | $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName); 62 | $results[] = $quotedColumnName; 63 | } 64 | return implode(',', $results); 65 | } 66 | 67 | public function getInsert(ReflectedTable $table, array $columnValues): string 68 | { 69 | $columns = array(); 70 | $values = array(); 71 | foreach ($columnValues as $columnName => $columnValue) { 72 | $column = $table->getColumn($columnName); 73 | $quotedColumnName = $this->quoteColumnName($column); 74 | $columns[] = $quotedColumnName; 75 | $columnValue = $this->converter->convertColumnValue($column); 76 | $values[] = $columnValue; 77 | } 78 | $columnsSql = '(' . implode(',', $columns) . ')'; 79 | $valuesSql = '(' . implode(',', $values) . ')'; 80 | $outputColumn = $this->quoteColumnName($table->getPk()); 81 | switch ($this->driver) { 82 | case 'mysql': 83 | return "$columnsSql VALUES $valuesSql"; 84 | case 'pgsql': 85 | return "$columnsSql VALUES $valuesSql RETURNING $outputColumn"; 86 | case 'sqlsrv': 87 | return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql"; 88 | case 'sqlite': 89 | return "$columnsSql VALUES $valuesSql"; 90 | } 91 | } 92 | 93 | public function getUpdate(ReflectedTable $table, array $columnValues): string 94 | { 95 | $results = array(); 96 | foreach ($columnValues as $columnName => $columnValue) { 97 | $column = $table->getColumn($columnName); 98 | $quotedColumnName = $this->quoteColumnName($column); 99 | $columnValue = $this->converter->convertColumnValue($column); 100 | $results[] = $quotedColumnName . '=' . $columnValue; 101 | } 102 | return implode(',', $results); 103 | } 104 | 105 | public function getIncrement(ReflectedTable $table, array $columnValues): string 106 | { 107 | $results = array(); 108 | foreach ($columnValues as $columnName => $columnValue) { 109 | if (!is_numeric($columnValue)) { 110 | continue; 111 | } 112 | $column = $table->getColumn($columnName); 113 | $quotedColumnName = $this->quoteColumnName($column); 114 | $columnValue = $this->converter->convertColumnValue($column); 115 | $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue; 116 | } 117 | return implode(',', $results); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Database/DataConverter.php: -------------------------------------------------------------------------------- 1 | driver = $driver; 15 | } 16 | 17 | private function convertRecordValue($conversion, $value) 18 | { 19 | $args = explode('|', $conversion); 20 | $type = array_shift($args); 21 | switch ($type) { 22 | case 'boolean': 23 | return $value ? true : false; 24 | case 'integer': 25 | return (int) $value; 26 | case 'float': 27 | return (float) $value; 28 | case 'decimal': 29 | return number_format($value, $args[0], '.', ''); 30 | } 31 | return $value; 32 | } 33 | 34 | private function getRecordValueConversion(ReflectedColumn $column): string 35 | { 36 | if ($column->isBoolean()) { 37 | return 'boolean'; 38 | } 39 | if (in_array($column->getType(), ['integer', 'bigint'])) { 40 | return 'integer'; 41 | } 42 | if (in_array($column->getType(), ['float', 'double'])) { 43 | return 'float'; 44 | } 45 | if (in_array($this->driver, ['sqlite']) && in_array($column->getType(), ['decimal'])) { 46 | return 'decimal|' . $column->getScale(); 47 | } 48 | return 'none'; 49 | } 50 | 51 | public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/ 52 | { 53 | foreach ($columnNames as $columnName) { 54 | $column = $table->getColumn($columnName); 55 | $conversion = $this->getRecordValueConversion($column); 56 | if ($conversion != 'none') { 57 | foreach ($records as $i => $record) { 58 | $value = $records[$i][$columnName]; 59 | if ($value === null) { 60 | continue; 61 | } 62 | $records[$i][$columnName] = $this->convertRecordValue($conversion, $value); 63 | } 64 | } 65 | } 66 | } 67 | 68 | private function convertInputValue($conversion, $value) 69 | { 70 | switch ($conversion) { 71 | case 'boolean': 72 | return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0; 73 | case 'base64url_to_base64': 74 | return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT); 75 | } 76 | return $value; 77 | } 78 | 79 | private function getInputValueConversion(ReflectedColumn $column): string 80 | { 81 | if ($column->isBoolean()) { 82 | return 'boolean'; 83 | } 84 | if ($column->isBinary()) { 85 | return 'base64url_to_base64'; 86 | } 87 | return 'none'; 88 | } 89 | 90 | public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/ 91 | { 92 | $columnNames = array_keys($columnValues); 93 | foreach ($columnNames as $columnName) { 94 | $column = $table->getColumn($columnName); 95 | $conversion = $this->getInputValueConversion($column); 96 | if ($conversion != 'none') { 97 | $value = $columnValues[$columnName]; 98 | if ($value !== null) { 99 | $columnValues[$columnName] = $this->convertInputValue($conversion, $value); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Database/LazyPdo.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn; 18 | $this->user = $user; 19 | $this->password = $password; 20 | $this->options = $options; 21 | $this->commands = array(); 22 | // explicitly NOT calling super::__construct 23 | } 24 | 25 | public function addInitCommand(string $command) /*: void*/ 26 | { 27 | $this->commands[] = $command; 28 | } 29 | 30 | private function pdo() 31 | { 32 | if (!$this->pdo) { 33 | $this->pdo = new \PDO($this->dsn, $this->user, $this->password, $this->options); 34 | foreach ($this->commands as $command) { 35 | $this->pdo->query($command); 36 | } 37 | } 38 | return $this->pdo; 39 | } 40 | 41 | public function reconstruct(string $dsn, /*?string*/ $user = null, /*?string*/ $password = null, array $options = array()): bool 42 | { 43 | $this->dsn = $dsn; 44 | $this->user = $user; 45 | $this->password = $password; 46 | $this->options = $options; 47 | $this->commands = array(); 48 | if ($this->pdo) { 49 | $this->pdo = null; 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | public function inTransaction(): bool 56 | { 57 | // Do not call parent method if there is no pdo object 58 | return $this->pdo && parent::inTransaction(); 59 | } 60 | 61 | public function setAttribute($attribute, $value): bool 62 | { 63 | if ($this->pdo) { 64 | return $this->pdo()->setAttribute($attribute, $value); 65 | } 66 | $this->options[$attribute] = $value; 67 | return true; 68 | } 69 | 70 | public function getAttribute($attribute): mixed 71 | { 72 | return $this->pdo()->getAttribute($attribute); 73 | } 74 | 75 | public function beginTransaction(): bool 76 | { 77 | return $this->pdo()->beginTransaction(); 78 | } 79 | 80 | public function commit(): bool 81 | { 82 | return $this->pdo()->commit(); 83 | } 84 | 85 | public function rollBack(): bool 86 | { 87 | return $this->pdo()->rollBack(); 88 | } 89 | 90 | #[\ReturnTypeWillChange] 91 | public function errorCode() 92 | { 93 | return $this->pdo()->errorCode(); 94 | } 95 | 96 | public function errorInfo(): array 97 | { 98 | return $this->pdo()->errorInfo(); 99 | } 100 | 101 | public function exec($query): int 102 | { 103 | return $this->pdo()->exec($query); 104 | } 105 | 106 | #[\ReturnTypeWillChange] 107 | public function prepare($statement, $options = array()) 108 | { 109 | return $this->pdo()->prepare($statement, $options); 110 | } 111 | 112 | public function quote($string, $parameter_type = \PDO::PARAM_STR): string 113 | { 114 | return $this->pdo()->quote($string, $parameter_type); 115 | } 116 | 117 | public function lastInsertId( /* ?string */$name = null): string 118 | { 119 | return $this->pdo()->lastInsertId($name); 120 | } 121 | 122 | public function query($query, /* ?int */ $fetchMode = null, ...$fetchModeArgs): \PDOStatement 123 | { 124 | return call_user_func_array(array($this->pdo(), 'query'), func_get_args()); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Database/RealNameMapper.php: -------------------------------------------------------------------------------- 1 | tableMapping = []; 15 | $this->reverseTableMapping = []; 16 | $this->columnMapping = []; 17 | $this->reverseColumnMapping = []; 18 | foreach ($mapping as $name=>$realName) { 19 | if (strpos($name,'.') && strpos($realName,'.')) { 20 | list($tableName, $columnName) = explode('.', $name, 2); 21 | list($tableRealName, $columnRealName) = explode('.', $realName, 2); 22 | $this->tableMapping[$tableName] = $tableRealName; 23 | $this->reverseTableMapping[$tableRealName] = $tableName; 24 | if (!isset($this->columnMapping[$tableName])) { 25 | $this->columnMapping[$tableName] = []; 26 | } 27 | $this->columnMapping[$tableName][$columnName] = $columnRealName; 28 | if (!isset($this->reverseColumnMapping[$tableRealName])) { 29 | $this->reverseColumnMapping[$tableRealName] = []; 30 | } 31 | $this->reverseColumnMapping[$tableRealName][$columnRealName] = $columnName; 32 | } else { 33 | $this->tableMapping[$name] = $realName; 34 | $this->reverseTableMapping[$realName] = $name; 35 | } 36 | } 37 | } 38 | 39 | public function getColumnRealName(string $tableName,string $columnName): string 40 | { 41 | return $this->reverseColumnMapping[$tableName][$columnName] ?? $columnName; 42 | } 43 | 44 | public function getTableRealName(string $tableName): string 45 | { 46 | return $this->reverseTableMapping[$tableName] ?? $tableName; 47 | } 48 | 49 | public function getColumnName(string $tableRealName,string $columnRealName): string 50 | { 51 | return $this->columnMapping[$tableRealName][$columnRealName] ?? $columnRealName; 52 | } 53 | 54 | public function getTableName(string $tableRealName): string 55 | { 56 | return $this->tableMapping[$tableRealName] ?? $tableRealName; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/GeoJson/Feature.php: -------------------------------------------------------------------------------- 1 | id = $id; 14 | $this->properties = $properties; 15 | $this->geometry = $geometry; 16 | } 17 | 18 | public function serialize() 19 | { 20 | return [ 21 | 'type' => 'Feature', 22 | 'id' => $this->id, 23 | 'properties' => $this->properties, 24 | 'geometry' => $this->geometry, 25 | ]; 26 | } 27 | 28 | #[\ReturnTypeWillChange] 29 | public function jsonSerialize() 30 | { 31 | return $this->serialize(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/GeoJson/FeatureCollection.php: -------------------------------------------------------------------------------- 1 | features = $features; 14 | $this->results = $results; 15 | } 16 | 17 | public function serialize() 18 | { 19 | return [ 20 | 'type' => 'FeatureCollection', 21 | 'features' => $this->features, 22 | 'results' => $this->results, 23 | ]; 24 | } 25 | 26 | #[\ReturnTypeWillChange] 27 | public function jsonSerialize() 28 | { 29 | return array_filter($this->serialize(), function ($v) { 30 | return $v !== -1; 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/GeoJson/GeoJsonService.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 17 | $this->records = $records; 18 | } 19 | 20 | public function hasTable(string $table): bool 21 | { 22 | return $this->reflection->hasTable($table); 23 | } 24 | 25 | public function getType(string $table): string 26 | { 27 | return $this->reflection->getType($table); 28 | } 29 | 30 | private function getGeometryColumnName(string $tableName, array &$params): string 31 | { 32 | $geometryParam = isset($params['geometry']) ? $params['geometry'][0] : ''; 33 | $table = $this->reflection->getTable($tableName); 34 | $geometryColumnName = ''; 35 | foreach ($table->getColumnNames() as $columnName) { 36 | if ($geometryParam && $geometryParam != $columnName) { 37 | continue; 38 | } 39 | $column = $table->getColumn($columnName); 40 | if ($column->isGeometry()) { 41 | $geometryColumnName = $columnName; 42 | break; 43 | } 44 | } 45 | if ($geometryColumnName) { 46 | $params['mandatory'][] = $tableName . "." . $geometryColumnName; 47 | } 48 | return $geometryColumnName; 49 | } 50 | 51 | private function setBoudingBoxFilter(string $geometryColumnName, array &$params) 52 | { 53 | $boundingBox = isset($params['bbox']) ? $params['bbox'][0] : ''; 54 | if ($boundingBox) { 55 | $c = explode(',', $boundingBox); 56 | if (!isset($params['filter'])) { 57 | $params['filter'] = array(); 58 | } 59 | $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))"; 60 | } 61 | $tile = isset($params['tile']) ? $params['tile'][0] : ''; 62 | if ($tile) { 63 | $zxy = explode(',', $tile); 64 | if (count($zxy) == 3) { 65 | list($z, $x, $y) = $zxy; 66 | $c = array(); 67 | $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x, $y)); 68 | $c = array_merge($c, $this->convertTileToLatLonOfUpperLeftCorner($z, $x + 1, $y + 1)); 69 | $params['filter'][] = "$geometryColumnName,sin,POLYGON(($c[0] $c[1],$c[2] $c[1],$c[2] $c[3],$c[0] $c[3],$c[0] $c[1]))"; 70 | } 71 | } 72 | } 73 | 74 | private function convertTileToLatLonOfUpperLeftCorner($z, $x, $y): array 75 | { 76 | $n = pow(2, $z); 77 | $lon = $x / $n * 360.0 - 180.0; 78 | $lat = rad2deg(atan(sinh(pi() * (1 - 2 * $y / $n)))); 79 | return [$lon, $lat]; 80 | } 81 | 82 | private function convertRecordToFeature(/*object*/$record, string $primaryKeyColumnName, string $geometryColumnName) 83 | { 84 | $id = null; 85 | if ($primaryKeyColumnName) { 86 | $id = $record[$primaryKeyColumnName]; 87 | } 88 | $geometry = null; 89 | if (isset($record[$geometryColumnName])) { 90 | $geometry = Geometry::fromWkt($record[$geometryColumnName]); 91 | } 92 | $properties = array_diff_key($record, [$primaryKeyColumnName => true, $geometryColumnName => true]); 93 | return new Feature($id, $properties, $geometry); 94 | } 95 | 96 | private function getPrimaryKeyColumnName(string $tableName, array &$params): string 97 | { 98 | $primaryKeyColumn = $this->reflection->getTable($tableName)->getPk(); 99 | if (!$primaryKeyColumn) { 100 | return ''; 101 | } 102 | $primaryKeyColumnName = $primaryKeyColumn->getName(); 103 | $params['mandatory'][] = $tableName . "." . $primaryKeyColumnName; 104 | return $primaryKeyColumnName; 105 | } 106 | 107 | public function _list(string $tableName, array $params): FeatureCollection 108 | { 109 | $geometryColumnName = $this->getGeometryColumnName($tableName, $params); 110 | $this->setBoudingBoxFilter($geometryColumnName, $params); 111 | $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params); 112 | $records = $this->records->_list($tableName, $params); 113 | $features = array(); 114 | foreach ($records->getRecords() as $record) { 115 | $features[] = $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName); 116 | } 117 | return new FeatureCollection($features, $records->getResults()); 118 | } 119 | 120 | public function read(string $tableName, string $id, array $params): Feature 121 | { 122 | $geometryColumnName = $this->getGeometryColumnName($tableName, $params); 123 | $primaryKeyColumnName = $this->getPrimaryKeyColumnName($tableName, $params); 124 | $record = $this->records->read($tableName, $id, $params); 125 | return $this->convertRecordToFeature($record, $primaryKeyColumnName, $geometryColumnName); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/GeoJson/Geometry.php: -------------------------------------------------------------------------------- 1 | type = $type; 23 | $this->coordinates = $coordinates; 24 | } 25 | 26 | public static function fromWkt(string $wkt): Geometry 27 | { 28 | $bracket = strpos($wkt, '('); 29 | $type = strtoupper(trim(substr($wkt, 0, $bracket))); 30 | $supported = false; 31 | foreach (Geometry::$types as $typeName) { 32 | if (strtoupper($typeName) == $type) { 33 | $type = $typeName; 34 | $supported = true; 35 | } 36 | } 37 | if (!$supported) { 38 | throw new \Exception('Geometry type not supported: ' . $type); 39 | } 40 | $coordinates = substr($wkt, $bracket); 41 | if (substr($type, -5) != 'Point' || ($type == 'MultiPoint' && $coordinates[1] != '(')) { 42 | $coordinates = preg_replace('|([0-9\-\.]+ )+([0-9\-\.]+)|', '[\1\2]', $coordinates); 43 | } 44 | $coordinates = str_replace(['(', ')', ', ', ' '], ['[', ']', ',', ','], $coordinates); 45 | $coordinates = json_decode($coordinates); 46 | if (!$coordinates) { 47 | throw new \Exception('Could not decode WKT: ' . $wkt); 48 | } 49 | return new Geometry($type, $coordinates); 50 | } 51 | 52 | public function serialize() 53 | { 54 | return [ 55 | 'type' => $this->type, 56 | 'coordinates' => $this->coordinates, 57 | ]; 58 | } 59 | 60 | #[\ReturnTypeWillChange] 61 | public function jsonSerialize() 62 | { 63 | return $this->serialize(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php: -------------------------------------------------------------------------------- 1 | getMethod(); 18 | $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET'); 19 | if (!in_array($method, $excludeMethods)) { 20 | $headerName = $this->getProperty('headerName', 'X-Requested-With'); 21 | $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest'); 22 | if ($headerValue != RequestUtils::getHeader($request, $headerName)) { 23 | return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method); 24 | } 25 | } 26 | return $next->handle($request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/ApiKeyAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | getProperty('header', 'X-API-Key'); 18 | $apiKey = RequestUtils::getHeader($request, $headerName); 19 | if ($apiKey) { 20 | $apiKeys = $this->getArrayProperty('keys', ''); 21 | if (!in_array($apiKey, $apiKeys)) { 22 | return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $apiKey); 23 | } 24 | } else { 25 | $authenticationMode = $this->getProperty('mode', 'required'); 26 | if ($authenticationMode == 'required') { 27 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 28 | } 29 | } 30 | $_SESSION['apiKey'] = $apiKey; 31 | return $next->handle($request); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/ApiKeyDbAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 29 | $this->db = $db; 30 | $this->ordering = new OrderingInfo(); 31 | } 32 | 33 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 34 | { 35 | $user = false; 36 | $headerName = $this->getProperty('header', 'X-API-Key'); 37 | $apiKey = RequestUtils::getHeader($request, $headerName); 38 | if ($apiKey) { 39 | $tableName = $this->getProperty('usersTable', 'users'); 40 | $table = $this->reflection->getTable($tableName); 41 | $apiKeyColumnName = $this->getProperty('apiKeyColumn', 'api_key'); 42 | $apiKeyColumn = $table->getColumn($apiKeyColumnName); 43 | $condition = new ColumnCondition($apiKeyColumn, 'eq', $apiKey); 44 | $columnNames = $table->getColumnNames(); 45 | $columnOrdering = $this->ordering->getDefaultColumnOrdering($table); 46 | $users = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, 0, 1); 47 | if (count($users) < 1) { 48 | return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $apiKey); 49 | } 50 | $user = $users[0]; 51 | } else { 52 | $authenticationMode = $this->getProperty('mode', 'required'); 53 | if ($authenticationMode == 'required') { 54 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 55 | } 56 | } 57 | $_SESSION['apiUser'] = $user; 58 | return $next->handle($request); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 26 | } 27 | 28 | private function handleColumns(string $operation, string $tableName) /*: void*/ 29 | { 30 | $columnHandler = $this->getProperty('columnHandler', ''); 31 | if ($columnHandler) { 32 | $table = $this->reflection->getTable($tableName); 33 | foreach ($table->getColumnNames() as $columnName) { 34 | $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName); 35 | if (!$allowed) { 36 | $table->removeColumn($columnName); 37 | } 38 | } 39 | } 40 | } 41 | 42 | private function handleTable(string $operation, string $tableName) /*: void*/ 43 | { 44 | if (!$this->reflection->hasTable($tableName)) { 45 | return; 46 | } 47 | $allowed = true; 48 | $tableHandler = $this->getProperty('tableHandler', ''); 49 | if ($tableHandler) { 50 | $allowed = call_user_func($tableHandler, $operation, $tableName); 51 | } 52 | if (!$allowed) { 53 | $this->reflection->removeTable($tableName); 54 | } else { 55 | $this->handleColumns($operation, $tableName); 56 | } 57 | } 58 | 59 | private function handleRecords(string $operation, string $tableName) /*: void*/ 60 | { 61 | if (!$this->reflection->hasTable($tableName)) { 62 | return; 63 | } 64 | $recordHandler = $this->getProperty('recordHandler', ''); 65 | if ($recordHandler) { 66 | $query = call_user_func($recordHandler, $operation, $tableName); 67 | $filters = new FilterInfo(); 68 | $table = $this->reflection->getTable($tableName); 69 | $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query ?: '')); 70 | parse_str($query, $params); 71 | $condition = $filters->getCombinedConditions($table, $params); 72 | VariableStore::set("authorization.conditions.$tableName", $condition); 73 | } 74 | } 75 | 76 | private function pathHandler(string $path) /*: bool*/ 77 | { 78 | $pathHandler = $this->getProperty('pathHandler', ''); 79 | return $pathHandler ? call_user_func($pathHandler, $path) : true; 80 | } 81 | 82 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 83 | { 84 | $path = RequestUtils::getPathSegment($request, 1); 85 | 86 | if (!$this->pathHandler($path)) { 87 | return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath()); 88 | } 89 | 90 | $operation = RequestUtils::getOperation($request); 91 | $tableNames = RequestUtils::getTableNames($request, $this->reflection); 92 | foreach ($tableNames as $tableName) { 93 | $this->handleTable($operation, $tableName); 94 | if ($path == 'records') { 95 | $this->handleRecords($operation, $tableName); 96 | } 97 | } 98 | if ($path == 'openapi') { 99 | VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', '')); 100 | VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', '')); 101 | } 102 | return $next->handle($request); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php: -------------------------------------------------------------------------------- 1 | load($this); 20 | $this->responder = $responder; 21 | $this->middleware = $middleware; 22 | $this->config = $config; 23 | } 24 | 25 | protected function getArrayProperty(string $key, string $default): array 26 | { 27 | return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default)))); 28 | } 29 | 30 | protected function getMapProperty(string $key, string $default): array 31 | { 32 | $pairs = $this->getArrayProperty($key, $default); 33 | $result = array(); 34 | foreach ($pairs as $pair) { 35 | if (strpos($pair, ':')) { 36 | list($k, $v) = explode(':', $pair, 2); 37 | $result[trim($k)] = trim($v); 38 | } else { 39 | $result[] = trim($pair); 40 | } 41 | } 42 | return $result; 43 | } 44 | 45 | protected function getProperty(string $key, $default) 46 | { 47 | return $this->config->getProperty($this->middleware . '.' . $key, $default); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | readPasswords($passwordFile); 30 | $valid = $this->hasCorrectPassword($username, $password, $passwords); 31 | $this->writePasswords($passwordFile, $passwords); 32 | return $valid ? $username : ''; 33 | } 34 | 35 | private function readPasswords(string $passwordFile): array 36 | { 37 | $passwords = []; 38 | $passwordLines = file($passwordFile); 39 | foreach ($passwordLines as $passwordLine) { 40 | if (strpos($passwordLine, ':') !== false) { 41 | list($username, $hash) = explode(':', trim($passwordLine), 2); 42 | if (strlen($hash) > 0 && $hash[0] != '$') { 43 | $hash = password_hash($hash, PASSWORD_DEFAULT); 44 | } 45 | $passwords[$username] = $hash; 46 | } 47 | } 48 | return $passwords; 49 | } 50 | 51 | private function writePasswords(string $passwordFile, array $passwords): bool 52 | { 53 | $success = false; 54 | $passwordFileContents = ''; 55 | foreach ($passwords as $username => $hash) { 56 | $passwordFileContents .= "$username:$hash\n"; 57 | } 58 | if (file_get_contents($passwordFile) != $passwordFileContents) { 59 | $success = file_put_contents($passwordFile, $passwordFileContents) !== false; 60 | } 61 | return $success; 62 | } 63 | 64 | private function getAuthorizationCredentials(ServerRequestInterface $request): string 65 | { 66 | $serverParams = $request->getServerParams(); 67 | if (isset($serverParams['PHP_AUTH_USER'])) { 68 | return $serverParams['PHP_AUTH_USER'] . ':' . $serverParams['PHP_AUTH_PW']; 69 | } 70 | $header = RequestUtils::getHeader($request, 'Authorization'); 71 | $parts = explode(' ', trim($header), 2); 72 | if (count($parts) != 2) { 73 | return ''; 74 | } 75 | if ($parts[0] != 'Basic') { 76 | return ''; 77 | } 78 | return base64_decode(strtr($parts[1], '-_', '+/')); 79 | } 80 | 81 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 82 | { 83 | if (session_status() == PHP_SESSION_NONE) { 84 | if (!headers_sent()) { 85 | $sessionName = $this->getProperty('sessionName', ''); 86 | if ($sessionName) { 87 | session_name($sessionName); 88 | } 89 | if (!ini_get('session.cookie_samesite')) { 90 | ini_set('session.cookie_samesite', 'Lax'); 91 | } 92 | if (!ini_get('session.cookie_httponly')) { 93 | ini_set('session.cookie_httponly', 1); 94 | } 95 | if (!ini_get('session.cookie_secure') && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { 96 | ini_set('session.cookie_secure', 1); 97 | } 98 | session_start(); 99 | } 100 | } 101 | $credentials = $this->getAuthorizationCredentials($request); 102 | if ($credentials) { 103 | list($username, $password) = array('', ''); 104 | if (strpos($credentials, ':') !== false) { 105 | list($username, $password) = explode(':', $credentials, 2); 106 | } 107 | $passwordFile = $this->getProperty('passwordFile', '.htpasswd'); 108 | $validUser = $this->getValidUsername($username, $password, $passwordFile); 109 | $_SESSION['username'] = $validUser; 110 | if (!$validUser) { 111 | return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username); 112 | } 113 | if (!headers_sent()) { 114 | session_regenerate_id(); 115 | } 116 | } 117 | if (!isset($_SESSION['username']) || !$_SESSION['username']) { 118 | $authenticationMode = $this->getProperty('mode', 'required'); 119 | if ($authenticationMode == 'required') { 120 | $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 121 | $realm = $this->getProperty('realm', 'Username and password required'); 122 | $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\""); 123 | return $response; 124 | } 125 | } 126 | return $next->handle($request); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php: -------------------------------------------------------------------------------- 1 | debug = $config->getDebug(); 24 | } 25 | 26 | private function isOriginAllowed(string $origin, string $allowedOrigins): bool 27 | { 28 | $found = false; 29 | foreach (explode(',', $allowedOrigins) as $allowedOrigin) { 30 | $hostname = preg_quote(strtolower(trim($allowedOrigin)), '/'); 31 | $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/'; 32 | if (preg_match($regex, $origin)) { 33 | $found = true; 34 | break; 35 | } 36 | } 37 | return $found; 38 | } 39 | 40 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 41 | { 42 | $method = $request->getMethod(); 43 | $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : ''; 44 | $allowedOrigins = $this->getProperty('allowedOrigins', '*'); 45 | if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) { 46 | $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin); 47 | } elseif ($method == 'OPTIONS') { 48 | $response = ResponseFactory::fromStatus(ResponseFactory::OK); 49 | $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization, X-API-Key'); 50 | if ($this->debug) { 51 | $allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File'])); 52 | } 53 | if ($allowHeaders) { 54 | $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders); 55 | } 56 | $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH'); 57 | if ($allowMethods) { 58 | $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods); 59 | } 60 | $allowCredentials = $this->getProperty('allowCredentials', 'true'); 61 | if ($allowCredentials) { 62 | $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials); 63 | } 64 | $maxAge = $this->getProperty('maxAge', '1728000'); 65 | if ($maxAge) { 66 | $response = $response->withHeader('Access-Control-Max-Age', $maxAge); 67 | } 68 | $exposeHeaders = $this->getProperty('exposeHeaders', ''); 69 | if ($this->debug) { 70 | $exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File'])); 71 | } 72 | if ($exposeHeaders) { 73 | $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders); 74 | } 75 | } else { 76 | $response = null; 77 | try { 78 | $response = $next->handle($request); 79 | } catch (\Throwable $e) { 80 | $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage()); 81 | if ($this->debug) { 82 | $response = ResponseUtils::addExceptionHeaders($response, $e); 83 | } 84 | } 85 | } 86 | if ($origin) { 87 | $allowCredentials = $this->getProperty('allowCredentials', 'true'); 88 | if ($allowCredentials) { 89 | $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials); 90 | } 91 | $response = $response->withHeader('Access-Control-Allow-Origin', $origin); 92 | } 93 | return $response; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 23 | } 24 | 25 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 26 | { 27 | $operation = RequestUtils::getOperation($request); 28 | $tableName = RequestUtils::getPathSegment($request, 2); 29 | $beforeHandler = $this->getProperty('beforeHandler', ''); 30 | $environment = (object) array(); 31 | if ($beforeHandler !== '') { 32 | $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment); 33 | $request = $result ?: $request; 34 | } 35 | $response = $next->handle($request); 36 | $afterHandler = $this->getProperty('afterHandler', ''); 37 | if ($afterHandler !== '') { 38 | $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment); 39 | $response = $result ?: $response; 40 | } 41 | return $response; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php: -------------------------------------------------------------------------------- 1 | ipMatch($ipAddress, $allowedIp)) { 33 | return true; 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | private function getIpAddress(ServerRequestInterface $request): string 40 | { 41 | $reverseProxy = $this->getProperty('reverseProxy', ''); 42 | if ($reverseProxy) { 43 | $ipAddress = array_pop($request->getHeader('X-Forwarded-For')); 44 | } else { 45 | $serverParams = $request->getServerParams(); 46 | $ipAddress = $serverParams['REMOTE_ADDR'] ?? '127.0.0.1'; 47 | } 48 | return $ipAddress; 49 | } 50 | 51 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 52 | { 53 | $ipAddress = $this->getIpAddress($request); 54 | $allowedIpAddresses = $this->getProperty('allowedIpAddresses', ''); 55 | if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) { 56 | $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, ''); 57 | } else { 58 | $response = $next->handle($request); 59 | } 60 | return $response; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 24 | } 25 | 26 | private function callHandler(ServerRequestInterface $request, $record, string $operation, ReflectedTable $table) /*: object */ 27 | { 28 | $context = (array) $record; 29 | $columnNames = $this->getProperty('columns', ''); 30 | if ($columnNames) { 31 | foreach (explode(',', $columnNames) as $columnName) { 32 | if ($table->hasColumn($columnName)) { 33 | if ($operation == 'create') { 34 | $context[$columnName] = $this->getIpAddress($request); 35 | } else { 36 | unset($context[$columnName]); 37 | } 38 | } 39 | } 40 | } 41 | return (object) $context; 42 | } 43 | 44 | private function getIpAddress(ServerRequestInterface $request): string 45 | { 46 | $reverseProxy = $this->getProperty('reverseProxy', ''); 47 | if ($reverseProxy) { 48 | $ipAddress = array_pop($request->getHeader('X-Forwarded-For')); 49 | } else { 50 | $serverParams = $request->getServerParams(); 51 | $ipAddress = $serverParams['REMOTE_ADDR'] ?? '127.0.0.1'; 52 | } 53 | return $ipAddress; 54 | } 55 | 56 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 57 | { 58 | $operation = RequestUtils::getOperation($request); 59 | if (in_array($operation, ['create', 'update', 'increment'])) { 60 | $tableNames = $this->getProperty('tables', ''); 61 | $tableName = RequestUtils::getPathSegment($request, 2); 62 | if (!$tableNames || in_array($tableName, explode(',', $tableNames))) { 63 | if ($this->reflection->hasTable($tableName)) { 64 | $record = $request->getParsedBody(); 65 | if ($record !== null) { 66 | $table = $this->reflection->getTable($tableName); 67 | if (is_array($record)) { 68 | foreach ($record as &$r) { 69 | $r = $this->callHandler($request, $r, $operation, $table); 70 | } 71 | } else { 72 | $record = $this->callHandler($request, $record, $operation, $table); 73 | } 74 | $request = $request->withParsedBody($record); 75 | } 76 | } 77 | } 78 | } 79 | return $next->handle($request); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 24 | } 25 | 26 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 27 | { 28 | $operation = RequestUtils::getOperation($request); 29 | $params = RequestUtils::getParams($request); 30 | if (in_array($operation, ['read', 'list']) && isset($params['join'])) { 31 | $maxDepth = (int) $this->getProperty('depth', '3'); 32 | $maxTables = (int) $this->getProperty('tables', '10'); 33 | $maxRecords = (int) $this->getProperty('records', '1000'); 34 | $tableCount = 0; 35 | $joinPaths = array(); 36 | for ($i = 0; $i < count($params['join']); $i++) { 37 | $joinPath = array(); 38 | $tables = explode(',', $params['join'][$i]); 39 | for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) { 40 | array_push($joinPath, $tables[$depth]); 41 | $tableCount += 1; 42 | if ($tableCount == $maxTables) { 43 | break; 44 | } 45 | } 46 | array_push($joinPaths, implode(',', $joinPath)); 47 | if ($tableCount == $maxTables) { 48 | break; 49 | } 50 | } 51 | $params['join'] = $joinPaths; 52 | $request = RequestUtils::setParams($request, $params); 53 | VariableStore::set("joinLimits.maxRecords", $maxRecords); 54 | } 55 | return $next->handle($request); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/JsonMiddleware.php: -------------------------------------------------------------------------------- 1 | $obj) { 26 | foreach ($obj as $k => $v) { 27 | if (in_array('all', $columnNames) || in_array($k, $columnNames)) { 28 | $object[$i]->$k = $this->convertJsonRequestValue($v); 29 | } 30 | } 31 | } 32 | } else if (is_object($object)) { 33 | foreach ($object as $k => $v) { 34 | if (in_array('all', $columnNames) || in_array($k, $columnNames)) { 35 | $object->$k = $this->convertJsonRequestValue($v); 36 | } 37 | } 38 | } 39 | return $object; 40 | } 41 | 42 | private function convertJsonResponseValue(string $value) /*: object */ 43 | { 44 | if (strlen($value) > 0 && in_array($value[0],['[','{'])) { 45 | $parsed = json_decode($value); 46 | if (json_last_error() == JSON_ERROR_NONE) { 47 | $value = $parsed; 48 | } 49 | } 50 | return $value; 51 | } 52 | 53 | 54 | private function convertJsonResponse($object, array $columnNames) /*: object */ 55 | { 56 | if (is_array($object)) { 57 | foreach ($object as $k => $v) { 58 | $object[$k] = $this->convertJsonResponse($v, $columnNames); 59 | } 60 | } else if (is_object($object)) { 61 | foreach ($object as $k => $v) { 62 | if (in_array('all', $columnNames) || in_array($k, $columnNames)) { 63 | $object->$k = $this->convertJsonResponse($v, $columnNames); 64 | } 65 | } 66 | } else if (is_string($object)) { 67 | $object = $this->convertJsonResponseValue($object); 68 | } 69 | return $object; 70 | } 71 | 72 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 73 | { 74 | $operation = RequestUtils::getOperation($request); 75 | $controllerPath = RequestUtils::getPathSegment($request, 1); 76 | $tableName = RequestUtils::getPathSegment($request, 2); 77 | 78 | $controllerPaths = $this->getArrayProperty('controllers', 'records,geojson'); 79 | $tableNames = $this->getArrayProperty('tables', 'all'); 80 | $columnNames = $this->getArrayProperty('columns', 'all'); 81 | if ( 82 | (in_array('all', $controllerPaths) || in_array($controllerPath, $controllerPaths)) && 83 | (in_array('all', $tableNames) || in_array($tableName, $tableNames)) 84 | ) { 85 | if (in_array($operation, ['create', 'update'])) { 86 | $records = $request->getParsedBody(); 87 | $records = $this->convertJsonRequest($records,$columnNames); 88 | $request = $request->withParsedBody($records); 89 | } 90 | $response = $next->handle($request); 91 | if (in_array($operation, ['read', 'list'])) { 92 | if ($response->getStatusCode() == ResponseFactory::OK) { 93 | $records = json_decode($response->getBody()->getContents()); 94 | $records = $this->convertJsonResponse($records, $columnNames); 95 | $response = $this->responder->success($records); 96 | } 97 | } 98 | } else { 99 | $response = $next->handle($request); 100 | } 101 | return $response; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | 'sha256', 18 | 'HS384' => 'sha384', 19 | 'HS512' => 'sha512', 20 | 'RS256' => 'sha256', 21 | 'RS384' => 'sha384', 22 | 'RS512' => 'sha512', 23 | ); 24 | $token = explode('.', $token); 25 | if (count($token) < 3) { 26 | return array(); 27 | } 28 | $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true); 29 | $kid = 0; 30 | if (isset($header['kid'])) { 31 | $kid = $header['kid']; 32 | } 33 | if (!isset($secrets[$kid])) { 34 | return array(); 35 | } 36 | $secret = $secrets[$kid]; 37 | if ($header['typ'] != 'JWT') { 38 | return array(); 39 | } 40 | $algorithm = $header['alg']; 41 | if (!isset($algorithms[$algorithm])) { 42 | return array(); 43 | } 44 | if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) { 45 | return array(); 46 | } 47 | $hmac = $algorithms[$algorithm]; 48 | $signature = base64_decode(strtr($token[2], '-_', '+/')); 49 | $data = "$token[0].$token[1]"; 50 | switch ($algorithm[0]) { 51 | case 'H': 52 | $hash = hash_hmac($hmac, $data, $secret, true); 53 | $equals = hash_equals($hash, $signature); 54 | if (!$equals) { 55 | return array(); 56 | } 57 | break; 58 | case 'R': 59 | $equals = openssl_verify($data, $signature, $secret, $hmac) == 1; 60 | if (!$equals) { 61 | return array(); 62 | } 63 | break; 64 | } 65 | $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true); 66 | if (!$claims) { 67 | return array(); 68 | } 69 | foreach ($requirements as $field => $values) { 70 | if (!empty($values)) { 71 | if ($field != 'alg') { 72 | if (!isset($claims[$field])) { 73 | return array(); 74 | } 75 | if (is_array($claims[$field])) { 76 | if (!array_intersect($claims[$field], $values)) { 77 | return array(); 78 | } 79 | } else { 80 | if (!in_array($claims[$field], $values)) { 81 | return array(); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) { 88 | return array(); 89 | } 90 | if (isset($claims['iat']) && $time + $leeway < $claims['iat']) { 91 | return array(); 92 | } 93 | if (isset($claims['exp']) && $time - $leeway > $claims['exp']) { 94 | return array(); 95 | } 96 | if (isset($claims['iat']) && !isset($claims['exp'])) { 97 | if ($time - $leeway > $claims['iat'] + $ttl) { 98 | return array(); 99 | } 100 | } 101 | return $claims; 102 | } 103 | 104 | private function getClaims(string $token): array 105 | { 106 | $time = (int) $this->getProperty('time', time()); 107 | $leeway = (int) $this->getProperty('leeway', '5'); 108 | $ttl = (int) $this->getProperty('ttl', '30'); 109 | $secrets = $this->getMapProperty('secrets', ''); 110 | if (!$secrets) { 111 | $secrets = [$this->getProperty('secret', '')]; 112 | } 113 | $requirements = array( 114 | 'alg' => $this->getArrayProperty('algorithms', ''), 115 | 'aud' => $this->getArrayProperty('audiences', ''), 116 | 'iss' => $this->getArrayProperty('issuers', ''), 117 | ); 118 | return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secrets, $requirements); 119 | } 120 | 121 | private function getAuthorizationToken(ServerRequestInterface $request): string 122 | { 123 | $headerName = $this->getProperty('header', 'X-Authorization'); 124 | $headerValue = RequestUtils::getHeader($request, $headerName); 125 | $parts = explode(' ', trim($headerValue), 2); 126 | if (count($parts) != 2) { 127 | return ''; 128 | } 129 | if ($parts[0] != 'Bearer') { 130 | return ''; 131 | } 132 | return $parts[1]; 133 | } 134 | 135 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 136 | { 137 | if (session_status() == PHP_SESSION_NONE) { 138 | if (!headers_sent()) { 139 | $sessionName = $this->getProperty('sessionName', ''); 140 | if ($sessionName) { 141 | session_name($sessionName); 142 | } 143 | if (!ini_get('session.cookie_samesite')) { 144 | ini_set('session.cookie_samesite', 'Lax'); 145 | } 146 | if (!ini_get('session.cookie_httponly')) { 147 | ini_set('session.cookie_httponly', 1); 148 | } 149 | if (!ini_get('session.cookie_secure') && isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') { 150 | ini_set('session.cookie_secure', 1); 151 | } 152 | session_start(); 153 | } 154 | } 155 | $token = $this->getAuthorizationToken($request); 156 | if ($token) { 157 | $claims = $this->getClaims($token); 158 | $_SESSION['claims'] = $claims; 159 | if (empty($claims)) { 160 | return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT'); 161 | } 162 | if (!headers_sent()) { 163 | session_regenerate_id(); 164 | } 165 | } 166 | if (empty($_SESSION['claims'])) { 167 | $authenticationMode = $this->getProperty('mode', 'required'); 168 | if ($authenticationMode == 'required') { 169 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 170 | } 171 | } 172 | return $next->handle($request); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 27 | } 28 | 29 | private function getCondition(string $tableName, array $pairs): Condition 30 | { 31 | $condition = new NoCondition(); 32 | $table = $this->reflection->getTable($tableName); 33 | foreach ($pairs as $k => $v) { 34 | $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v)); 35 | } 36 | return $condition; 37 | } 38 | 39 | private function getPairs($handler, string $operation, string $tableName): array 40 | { 41 | $result = array(); 42 | $pairs = call_user_func($handler, $operation, $tableName) ?: []; 43 | $table = $this->reflection->getTable($tableName); 44 | foreach ($pairs as $k => $v) { 45 | if ($table->hasColumn($k)) { 46 | $result[$k] = $v; 47 | } 48 | } 49 | return $result; 50 | } 51 | 52 | private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface 53 | { 54 | $record = $request->getParsedBody(); 55 | if ($record === null) { 56 | return $request; 57 | } 58 | $multi = is_array($record); 59 | $records = $multi ? $record : [$record]; 60 | foreach ($records as &$record) { 61 | foreach ($pairs as $column => $value) { 62 | if ($operation == 'create') { 63 | $record->$column = $value; 64 | } else { 65 | if (isset($record->$column)) { 66 | unset($record->$column); 67 | } 68 | } 69 | } 70 | } 71 | return $request->withParsedBody($multi ? $records : $records[0]); 72 | } 73 | 74 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 75 | { 76 | $handler = $this->getProperty('handler', ''); 77 | if ($handler !== '') { 78 | $path = RequestUtils::getPathSegment($request, 1); 79 | if ($path == 'records') { 80 | $operation = RequestUtils::getOperation($request); 81 | $tableNames = RequestUtils::getTableNames($request, $this->reflection); 82 | foreach ($tableNames as $i => $tableName) { 83 | if (!$this->reflection->hasTable($tableName)) { 84 | continue; 85 | } 86 | $pairs = $this->getPairs($handler, $operation, $tableName); 87 | if ($i == 0) { 88 | if (in_array($operation, ['create', 'update', 'increment'])) { 89 | $request = $this->handleRecord($request, $operation, $pairs); 90 | } 91 | } 92 | $condition = $this->getCondition($tableName, $pairs); 93 | VariableStore::set("multiTenancy.conditions.$tableName", $condition); 94 | } 95 | } 96 | } 97 | return $next->handle($request); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 24 | } 25 | 26 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 27 | { 28 | $operation = RequestUtils::getOperation($request); 29 | if ($operation == 'list') { 30 | $params = RequestUtils::getParams($request); 31 | $maxPage = (int) $this->getProperty('pages', '100'); 32 | if (isset($params['page']) && $params['page'] && $maxPage > 0) { 33 | if (strpos($params['page'][0], ',') === false) { 34 | $page = $params['page'][0]; 35 | } else { 36 | list($page, $size) = explode(',', $params['page'][0], 2); 37 | } 38 | if ($page > $maxPage) { 39 | return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, ''); 40 | } 41 | } 42 | $maxSize = (int) $this->getProperty('records', '1000'); 43 | if (!isset($params['size']) || !$params['size'] && $maxSize > 0) { 44 | $params['size'] = array($maxSize); 45 | } else { 46 | $params['size'] = array(min($params['size'][0], $maxSize)); 47 | } 48 | $request = RequestUtils::setParams($request, $params); 49 | } 50 | return $next->handle($request); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/ReconnectMiddleware.php: -------------------------------------------------------------------------------- 1 | config = $config; 24 | $this->db = $db; 25 | } 26 | 27 | private function getDriver(): string 28 | { 29 | $driverHandler = $this->getProperty('driverHandler', ''); 30 | if ($driverHandler) { 31 | return call_user_func($driverHandler); 32 | } 33 | return $this->config->getDriver(); 34 | } 35 | 36 | private function getAddress(): string 37 | { 38 | $addressHandler = $this->getProperty('addressHandler', ''); 39 | if ($addressHandler) { 40 | return call_user_func($addressHandler); 41 | } 42 | return $this->config->getAddress(); 43 | } 44 | 45 | private function getPort(): int 46 | { 47 | $portHandler = $this->getProperty('portHandler', ''); 48 | if ($portHandler) { 49 | return call_user_func($portHandler); 50 | } 51 | return $this->config->getPort(); 52 | } 53 | 54 | private function getDatabase(): string 55 | { 56 | $databaseHandler = $this->getProperty('databaseHandler', ''); 57 | if ($databaseHandler) { 58 | return call_user_func($databaseHandler); 59 | } 60 | return $this->config->getDatabase(); 61 | } 62 | 63 | private function getCommand(): string 64 | { 65 | $commandHandler = $this->getProperty('commandHandler', ''); 66 | if ($commandHandler) { 67 | return call_user_func($commandHandler); 68 | } 69 | return $this->config->getCommand(); 70 | } 71 | 72 | private function getTables(): array 73 | { 74 | $tablesHandler = $this->getProperty('tablesHandler', ''); 75 | if ($tablesHandler) { 76 | return call_user_func($tablesHandler); 77 | } 78 | return $this->config->getTables(); 79 | } 80 | 81 | private function getMapping(): array 82 | { 83 | $mappingHandler = $this->getProperty('mappingHandler', ''); 84 | if ($mappingHandler) { 85 | return call_user_func($mappingHandler); 86 | } 87 | return $this->config->getMapping(); 88 | } 89 | 90 | private function getUsername(): string 91 | { 92 | $usernameHandler = $this->getProperty('usernameHandler', ''); 93 | if ($usernameHandler) { 94 | return call_user_func($usernameHandler); 95 | } 96 | return $this->config->getUsername(); 97 | } 98 | 99 | private function getPassword(): string 100 | { 101 | $passwordHandler = $this->getProperty('passwordHandler', ''); 102 | if ($passwordHandler) { 103 | return call_user_func($passwordHandler); 104 | } 105 | return $this->config->getPassword(); 106 | } 107 | 108 | private function getGeometrySrid(): int 109 | { 110 | $geometrySridHandler = $this->getProperty('geometrySridHandler', ''); 111 | if ($geometrySridHandler) { 112 | return call_user_func($geometrySridHandler); 113 | } 114 | return $this->config->getGeometrySrid(); 115 | } 116 | 117 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 118 | { 119 | $driver = $this->getDriver(); 120 | $address = $this->getAddress(); 121 | $port = $this->getPort(); 122 | $database = $this->getDatabase(); 123 | $command = $this->getCommand(); 124 | $tables = $this->getTables(); 125 | $mapping = $this->getMapping(); 126 | $username = $this->getUsername(); 127 | $password = $this->getPassword(); 128 | $geometrySrid = $this->getGeometrySrid(); 129 | $this->db->reconstruct($driver, $address, $port, $database, $command, $tables, $mapping, $username, $password, $geometrySrid); 130 | return $next->handle($request); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/Router/Router.php: -------------------------------------------------------------------------------- 1 | basePath = rtrim($basePath, '/') ?: rtrim($this->detectBasePath(), '/');; 28 | $this->responder = $responder; 29 | $this->cache = $cache; 30 | $this->ttl = $ttl; 31 | $this->registration = true; 32 | $this->routes = $this->loadPathTree(); 33 | $this->routeHandlers = []; 34 | $this->middlewares = array(); 35 | } 36 | 37 | private function detectBasePath(): string 38 | { 39 | if (isset($_SERVER['REQUEST_URI'])) { 40 | $fullPath = urldecode(explode('?', $_SERVER['REQUEST_URI'])[0]); 41 | if (isset($_SERVER['PATH_INFO'])) { 42 | $path = $_SERVER['PATH_INFO']; 43 | if (substr($fullPath, -1 * strlen($path)) == $path) { 44 | return substr($fullPath, 0, -1 * strlen($path)); 45 | } 46 | } 47 | $path = '/' . basename(__FILE__); 48 | if (substr($fullPath, -1 * strlen($path)) == $path) { 49 | return $fullPath; 50 | } 51 | } 52 | return '/'; 53 | } 54 | 55 | private function loadPathTree(): PathTree 56 | { 57 | $data = $this->cache->get('PathTree'); 58 | if ($data != '') { 59 | $tree = PathTree::fromJson(json_decode(gzuncompress($data))); 60 | $this->registration = false; 61 | } else { 62 | $tree = new PathTree(); 63 | } 64 | return $tree; 65 | } 66 | 67 | public function register(string $method, string $path, array $handler) 68 | { 69 | $routeNumber = count($this->routeHandlers); 70 | $this->routeHandlers[$routeNumber] = $handler; 71 | if ($this->registration) { 72 | $path = trim($path, '/'); 73 | $parts = array(); 74 | if ($path) { 75 | $parts = explode('/', $path); 76 | } 77 | array_unshift($parts, $method); 78 | $this->routes->put($parts, $routeNumber); 79 | } 80 | } 81 | 82 | public function load(Middleware $middleware) /*: void*/ 83 | { 84 | array_push($this->middlewares, $middleware); 85 | } 86 | 87 | public function route(ServerRequestInterface $request): ResponseInterface 88 | { 89 | if ($this->registration) { 90 | $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE)); 91 | $this->cache->set('PathTree', $data, $this->ttl); 92 | } 93 | 94 | return $this->handle($request); 95 | } 96 | 97 | private function getRouteNumbers(ServerRequestInterface $request): array 98 | { 99 | $method = strtoupper($request->getMethod()); 100 | $path = array(); 101 | $segment = $method; 102 | for ($i = 1; strlen($segment) > 0; $i++) { 103 | array_push($path, $segment); 104 | $segment = RequestUtils::getPathSegment($request, $i); 105 | } 106 | return $this->routes->match($path); 107 | } 108 | 109 | private function removeBasePath(ServerRequestInterface $request): ServerRequestInterface 110 | { 111 | $path = $request->getUri()->getPath(); 112 | if (substr($path, 0, strlen($this->basePath)) == $this->basePath) { 113 | $path = substr($path, strlen($this->basePath)); 114 | $request = $request->withUri($request->getUri()->withPath($path)); 115 | } 116 | return $request; 117 | } 118 | 119 | public function getBasePath(): string 120 | { 121 | return $this->basePath; 122 | } 123 | 124 | public function handle(ServerRequestInterface $request): ResponseInterface 125 | { 126 | $request = $this->removeBasePath($request); 127 | 128 | if (count($this->middlewares)) { 129 | $handler = array_shift($this->middlewares); 130 | return $handler->process($request, $this); 131 | } 132 | 133 | $routeNumbers = $this->getRouteNumbers($request); 134 | if (count($routeNumbers) == 0) { 135 | return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath()); 136 | } 137 | try { 138 | $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request); 139 | } catch (\Throwable $exception) { 140 | $response = $this->responder->exception($exception); 141 | } 142 | return $response; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 25 | } 26 | 27 | private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */ 28 | { 29 | $context = (array) $record; 30 | $tableName = $table->getName(); 31 | foreach ($context as $columnName => &$value) { 32 | if ($table->hasColumn($columnName)) { 33 | $column = $table->getColumn($columnName); 34 | $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value); 35 | $value = $this->sanitizeType($table, $column, $value); 36 | } 37 | } 38 | return (object) $context; 39 | } 40 | 41 | private function sanitizeType(ReflectedTable $table, ReflectedColumn $column, $value) 42 | { 43 | $tables = $this->getArrayProperty('tables', 'all'); 44 | $types = $this->getArrayProperty('types', 'all'); 45 | if ( 46 | (in_array('all', $tables) || in_array($table->getName(), $tables)) && 47 | (in_array('all', $types) || in_array($column->getType(), $types)) 48 | ) { 49 | if (is_null($value)) { 50 | return $value; 51 | } 52 | if (is_string($value)) { 53 | $newValue = null; 54 | switch ($column->getType()) { 55 | case 'integer': 56 | case 'bigint': 57 | $newValue = filter_var(trim($value), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); 58 | break; 59 | case 'decimal': 60 | $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); 61 | if (is_float($newValue)) { 62 | $newValue = number_format($newValue, $column->getScale(), '.', ''); 63 | } 64 | break; 65 | case 'float': 66 | case 'double': 67 | $newValue = filter_var(trim($value), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); 68 | break; 69 | case 'boolean': 70 | $newValue = filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); 71 | break; 72 | case 'date': 73 | $time = strtotime(trim($value)); 74 | if ($time !== false) { 75 | $newValue = date('Y-m-d', $time); 76 | } 77 | break; 78 | case 'time': 79 | $time = strtotime(trim($value)); 80 | if ($time !== false) { 81 | $newValue = date('H:i:s', $time); 82 | } 83 | break; 84 | case 'timestamp': 85 | $time = strtotime(trim($value)); 86 | if ($time !== false) { 87 | $newValue = date('Y-m-d H:i:s', $time); 88 | } 89 | break; 90 | case 'blob': 91 | case 'varbinary': 92 | // allow base64url format 93 | $newValue = strtr(trim($value), '-_', '+/'); 94 | break; 95 | case 'clob': 96 | case 'varchar': 97 | $newValue = $value; 98 | break; 99 | case 'geometry': 100 | $newValue = trim($value); 101 | break; 102 | } 103 | if (!is_null($newValue)) { 104 | $value = $newValue; 105 | } 106 | } else { 107 | switch ($column->getType()) { 108 | case 'integer': 109 | case 'bigint': 110 | if (is_float($value)) { 111 | $value = (int) round($value); 112 | } 113 | break; 114 | case 'decimal': 115 | if (is_float($value) || is_int($value)) { 116 | $value = number_format((float) $value, $column->getScale(), '.', ''); 117 | } 118 | break; 119 | } 120 | } 121 | // post process 122 | } 123 | return $value; 124 | } 125 | 126 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 127 | { 128 | $operation = RequestUtils::getOperation($request); 129 | if (in_array($operation, ['create', 'update', 'increment'])) { 130 | $tableName = RequestUtils::getPathSegment($request, 2); 131 | if ($this->reflection->hasTable($tableName)) { 132 | $record = $request->getParsedBody(); 133 | if ($record !== null) { 134 | $handler = $this->getProperty('handler', ''); 135 | if ($handler !== '') { 136 | $table = $this->reflection->getTable($tableName); 137 | if (is_array($record)) { 138 | foreach ($record as &$r) { 139 | $r = $this->callHandler($handler, $r, $operation, $table); 140 | } 141 | } else { 142 | $record = $this->callHandler($handler, $record, $operation, $table); 143 | } 144 | $request = $request->withParsedBody($record); 145 | } 146 | } 147 | } 148 | } 149 | return $next->handle($request); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/SslRedirectMiddleware.php: -------------------------------------------------------------------------------- 1 | getUri(); 16 | $scheme = $uri->getScheme(); 17 | if ($scheme == 'http') { 18 | $uri = $request->getUri(); 19 | $uri = $uri->withScheme('https'); 20 | $response = ResponseFactory::fromStatus(301); 21 | $response = $response->withHeader('Location', $uri->__toString()); 22 | } else { 23 | $response = $next->handle($request); 24 | } 25 | return $response; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/TextSearchMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 23 | } 24 | 25 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 26 | { 27 | $operation = RequestUtils::getOperation($request); 28 | if ($operation == 'list') { 29 | $tableName = RequestUtils::getPathSegment($request, 2); 30 | $params = RequestUtils::getParams($request); 31 | $parameterName = $this->getProperty('parameter', 'search'); 32 | if (isset($params[$parameterName])) { 33 | $search = $params[$parameterName][0]; 34 | unset($params[$parameterName]); 35 | $table = $this->reflection->getTable($tableName); 36 | $i = 0; 37 | foreach ($table->getColumnNames() as $columnName) { 38 | $column = $table->getColumn($columnName); 39 | while (isset($params["filter$i"])) { 40 | $i++; 41 | } 42 | if ($i >= 10) { 43 | break; 44 | } 45 | if ($column->isText()) { 46 | $params["filter$i"] = "$columnName,cs,$search"; 47 | $i++; 48 | } 49 | } 50 | } 51 | $request = RequestUtils::setParams($request, $params); 52 | } 53 | return $next->handle($request); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php: -------------------------------------------------------------------------------- 1 | reflection = $reflection; 26 | } 27 | 28 | private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/ 29 | { 30 | $context = (array) $record; 31 | $details = array(); 32 | $tableName = $table->getName(); 33 | foreach ($context as $columnName => $value) { 34 | if ($table->hasColumn($columnName)) { 35 | $column = $table->getColumn($columnName); 36 | $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context); 37 | if ($valid === true || $valid === '') { 38 | $valid = $this->validateType($table, $column, $value); 39 | } 40 | if ($valid !== true && $valid !== '') { 41 | $details[$columnName] = $valid; 42 | } 43 | } 44 | } 45 | if (count($details) > 0) { 46 | return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details); 47 | } 48 | return null; 49 | } 50 | 51 | private function validateType(ReflectedTable $table, ReflectedColumn $column, $value) 52 | { 53 | $tables = $this->getArrayProperty('tables', 'all'); 54 | $types = $this->getArrayProperty('types', 'all'); 55 | if ( 56 | (in_array('all', $tables) || in_array($table->getName(), $tables)) && 57 | (in_array('all', $types) || in_array($column->getType(), $types)) 58 | ) { 59 | if (is_null($value)) { 60 | return ($column->getNullable() ? true : "cannot be null"); 61 | } 62 | if (is_string($value)) { 63 | // check for whitespace 64 | switch ($column->getType()) { 65 | case 'varchar': 66 | case 'clob': 67 | break; 68 | default: 69 | if (strlen(trim($value)) != strlen($value)) { 70 | return 'illegal whitespace'; 71 | } 72 | break; 73 | } 74 | // try to parse 75 | switch ($column->getType()) { 76 | case 'integer': 77 | case 'bigint': 78 | if ( 79 | filter_var($value, FILTER_SANITIZE_NUMBER_INT) !== $value || 80 | filter_var($value, FILTER_VALIDATE_INT) === false 81 | ) { 82 | return 'invalid integer'; 83 | } 84 | break; 85 | case 'decimal': 86 | if (strpos($value, '.') !== false) { 87 | list($whole, $decimals) = explode('.', ltrim($value, '-'), 2); 88 | } else { 89 | list($whole, $decimals) = array(ltrim($value, '-'), ''); 90 | } 91 | if (strlen($whole) > 0 && !ctype_digit($whole)) { 92 | return 'invalid decimal'; 93 | } 94 | if (strlen($decimals) > 0 && !ctype_digit($decimals)) { 95 | return 'invalid decimal'; 96 | } 97 | if (strlen($whole) > $column->getPrecision() - $column->getScale()) { 98 | return 'decimal too large'; 99 | } 100 | if (strlen($decimals) > $column->getScale()) { 101 | return 'decimal too precise'; 102 | } 103 | break; 104 | case 'float': 105 | case 'double': 106 | if ( 107 | filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT) !== $value || 108 | filter_var($value, FILTER_VALIDATE_FLOAT) === false 109 | ) { 110 | return 'invalid float'; 111 | } 112 | break; 113 | case 'boolean': 114 | if (!in_array(strtolower($value), array('true', 'false'))) { 115 | return 'invalid boolean'; 116 | } 117 | break; 118 | case 'date': 119 | if (date_create_from_format('Y-m-d', $value) === false) { 120 | return 'invalid date'; 121 | } 122 | break; 123 | case 'time': 124 | if (date_create_from_format('H:i:s', $value) === false) { 125 | return 'invalid time'; 126 | } 127 | break; 128 | case 'timestamp': 129 | if (date_create_from_format('Y-m-d H:i:s', $value) === false) { 130 | return 'invalid timestamp'; 131 | } 132 | break; 133 | case 'clob': 134 | case 'varchar': 135 | if ($column->hasLength() && mb_strlen($value, 'UTF-8') > $column->getLength()) { 136 | return 'string too long'; 137 | } 138 | break; 139 | case 'blob': 140 | case 'varbinary': 141 | if (base64_decode($value, true) === false) { 142 | return 'invalid base64'; 143 | } 144 | if ($column->hasLength() && strlen(base64_decode($value)) > $column->getLength()) { 145 | return 'string too long'; 146 | } 147 | break; 148 | case 'geometry': 149 | // no checks yet 150 | break; 151 | } 152 | } else { // check non-string types 153 | switch ($column->getType()) { 154 | case 'integer': 155 | case 'bigint': 156 | if (!is_int($value)) { 157 | return 'invalid integer'; 158 | } 159 | break; 160 | case 'float': 161 | case 'double': 162 | if (!is_float($value) && !is_int($value)) { 163 | return 'invalid float'; 164 | } 165 | break; 166 | case 'boolean': 167 | if (!is_bool($value) && ($value !== 0) && ($value !== 1)) { 168 | return 'invalid boolean'; 169 | } 170 | break; 171 | default: 172 | return 'invalid ' . $column->getType(); 173 | } 174 | } 175 | // extra checks 176 | switch ($column->getType()) { 177 | case 'integer': // 4 byte signed 178 | $value = filter_var($value, FILTER_VALIDATE_INT); 179 | if ($value > 2147483647 || $value < -2147483648) { 180 | return 'invalid integer'; 181 | } 182 | break; 183 | } 184 | } 185 | return (true); 186 | } 187 | 188 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 189 | { 190 | $operation = RequestUtils::getOperation($request); 191 | if (in_array($operation, ['create', 'update', 'increment'])) { 192 | $tableName = RequestUtils::getPathSegment($request, 2); 193 | if ($this->reflection->hasTable($tableName)) { 194 | $record = $request->getParsedBody(); 195 | if ($record !== null) { 196 | $handler = $this->getProperty('handler', ''); 197 | if ($handler !== '') { 198 | $table = $this->reflection->getTable($tableName); 199 | if (is_array($record)) { 200 | foreach ($record as $r) { 201 | $response = $this->callHandler($handler, $r, $operation, $table); 202 | if ($response !== null) { 203 | return $response; 204 | } 205 | } 206 | } else { 207 | $response = $this->callHandler($handler, $record, $operation, $table); 208 | if ($response !== null) { 209 | return $response; 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | return $next->handle($request); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/WpAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | getProperty('wpDirectory', '.'); 26 | require_once("$wpDirectory/wp-load.php"); 27 | $path = RequestUtils::getPathSegment($request, 1); 28 | $method = $request->getMethod(); 29 | if ($method == 'POST' && $path == 'login') { 30 | $body = $request->getParsedBody(); 31 | $usernameFormFieldName = $this->getProperty('usernameFormField', 'username'); 32 | $passwordFormFieldName = $this->getProperty('passwordFormField', 'password'); 33 | $username = isset($body->$usernameFormFieldName) ? $body->$usernameFormFieldName : ''; 34 | $password = isset($body->$passwordFormFieldName) ? $body->$passwordFormFieldName : ''; 35 | $user = wp_signon([ 36 | 'user_login' => $username, 37 | 'user_password' => $password, 38 | 'remember' => false, 39 | ]); 40 | if ($user->ID) { 41 | unset($user->data->user_pass); 42 | return $this->responder->success($user); 43 | } 44 | return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username); 45 | } 46 | if ($method == 'POST' && $path == 'logout') { 47 | if (is_user_logged_in()) { 48 | wp_logout(); 49 | $user = wp_get_current_user(); 50 | unset($user->data->user_pass); 51 | return $this->responder->success($user); 52 | } 53 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 54 | } 55 | if ($method == 'GET' && $path == 'me') { 56 | if (is_user_logged_in()) { 57 | $user = wp_get_current_user(); 58 | unset($user->data->user_pass); 59 | return $this->responder->success($user); 60 | } 61 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 62 | } 63 | if (!is_user_logged_in()) { 64 | $authenticationMode = $this->getProperty('mode', 'required'); 65 | if ($authenticationMode == 'required') { 66 | return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, ''); 67 | } 68 | } 69 | return $next->handle($request); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/XmlMiddleware.php: -------------------------------------------------------------------------------- 1 | createElement("root"); 27 | $d->appendChild($c); 28 | $t = function ($v) { 29 | $type = gettype($v); 30 | switch ($type) { 31 | case 'integer': 32 | return 'number'; 33 | case 'double': 34 | return 'number'; 35 | default: 36 | return strtolower($type); 37 | } 38 | }; 39 | $ts = explode(',', $types); 40 | $f = function ($f, $c, $a, $s = false) use ($t, $d, $ts) { 41 | if (in_array($t($a), $ts)) { 42 | $c->setAttribute('type', $t($a)); 43 | } 44 | if ($t($a) != 'array' && $t($a) != 'object') { 45 | if ($t($a) == 'boolean') { 46 | $c->appendChild($d->createTextNode($a ? 'true' : 'false')); 47 | } else { 48 | $c->appendChild($d->createTextNode($a)); 49 | } 50 | } else { 51 | foreach ($a as $k => $v) { 52 | $k = preg_replace('/[^a-z0-9\-\_\.]/', '', $k); 53 | if ($k == '__type' && $t($a) == 'object') { 54 | $c->setAttribute('__type', $v); 55 | } else { 56 | if ($t($v) == 'object') { 57 | $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k)); 58 | $f($f, $ch, $v); 59 | } else if ($t($v) == 'array') { 60 | $ch = $c->appendChild($d->createElementNS(null, $s ? 'item' : $k)); 61 | $f($f, $ch, $v, true); 62 | } else { 63 | $va = $d->createElementNS(null, $s ? 'item' : $k); 64 | if ($t($v) == 'boolean') { 65 | $va->appendChild($d->createTextNode($v ? 'true' : 'false')); 66 | } else { 67 | $va->appendChild($d->createTextNode((string) $v)); 68 | } 69 | $ch = $c->appendChild($va); 70 | if (in_array($t($v), $ts)) { 71 | $ch->setAttribute('type', $t($v)); 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }; 78 | $f($f, $c, $a, $t($a) == 'array'); 79 | return $d->saveXML($d->documentElement); 80 | } 81 | 82 | private function xml2json($xml): string 83 | { 84 | $o = @simplexml_load_string($xml); 85 | if ($o === false) { 86 | return ''; 87 | } 88 | $a = @dom_import_simplexml($o); 89 | if (!$a) { 90 | return ''; 91 | } 92 | $t = function ($v) { 93 | $t = $v->getAttribute('type'); 94 | $txt = $v->firstChild->nodeType == XML_TEXT_NODE; 95 | return $t ?: ($txt ? 'string' : 'object'); 96 | }; 97 | $f = function ($f, $a) use ($t) { 98 | $c = null; 99 | if ($t($a) == 'null') { 100 | $c = null; 101 | } else if ($t($a) == 'boolean') { 102 | $b = substr(strtolower($a->textContent), 0, 1); 103 | $c = in_array($b, array('1', 't')); 104 | } else if ($t($a) == 'number') { 105 | $c = $a->textContent + 0; 106 | } else if ($t($a) == 'string') { 107 | $c = $a->textContent; 108 | } else if ($t($a) == 'object') { 109 | $c = array(); 110 | if ($a->getAttribute('__type')) { 111 | $c['__type'] = $a->getAttribute('__type'); 112 | } 113 | for ($i = 0; $i < $a->childNodes->length; $i++) { 114 | $v = $a->childNodes[$i]; 115 | $c[$v->nodeName] = $f($f, $v); 116 | } 117 | $c = (object) $c; 118 | } else if ($t($a) == 'array') { 119 | $c = array(); 120 | for ($i = 0; $i < $a->childNodes->length; $i++) { 121 | $v = $a->childNodes[$i]; 122 | $c[$i] = $f($f, $v); 123 | } 124 | } 125 | return $c; 126 | }; 127 | $c = $f($f, $a); 128 | return (string) json_encode($c); 129 | } 130 | 131 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 132 | { 133 | parse_str($request->getUri()->getQuery(), $params); 134 | $isXml = isset($params['format']) && $params['format'] == 'xml'; 135 | if ($isXml) { 136 | $body = $request->getBody()->getContents(); 137 | if ($body) { 138 | $json = $this->xml2json($body); 139 | $request = $request->withParsedBody(json_decode($json)); 140 | } 141 | } 142 | $response = $next->handle($request); 143 | if ($isXml) { 144 | $body = $response->getBody()->getContents(); 145 | if ($body) { 146 | $types = implode(',', $this->getArrayProperty('types', 'null,array')); 147 | if ($types == '' || $types == 'all') { 148 | $xml = $this->json2xml($body); 149 | } else { 150 | $xml = $this->json2xml($body, $types); 151 | } 152 | $response = ResponseFactory::fromXml(ResponseFactory::OK, $xml); 153 | } 154 | } 155 | return $response; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php: -------------------------------------------------------------------------------- 1 | getProperty('cookieName', 'XSRF-TOKEN'); 17 | $cookieParams = $request->getCookieParams(); 18 | if (isset($cookieParams[$cookieName])) { 19 | $token = $cookieParams[$cookieName]; 20 | } else { 21 | $secure = $request->getUri()->getScheme() == 'https'; 22 | $token = bin2hex(random_bytes(8)); 23 | if (!headers_sent()) { 24 | setcookie($cookieName, $token, 0, '/', '', $secure); 25 | } 26 | } 27 | return $token; 28 | } 29 | 30 | public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface 31 | { 32 | $token = $this->getToken($request); 33 | $method = $request->getMethod(); 34 | $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET'); 35 | if (!in_array($method, $excludeMethods)) { 36 | $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN'); 37 | if ($token != $request->getHeader($headerName)[0]) { 38 | return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, ''); 39 | } 40 | } 41 | return $next->handle($request); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php: -------------------------------------------------------------------------------- 1 | openapi = new OpenApiDefinition($base); 20 | $this->records = in_array('records', $controllers) ? new OpenApiRecordsBuilder($this->openapi, $reflection) : null; 21 | $this->columns = in_array('columns', $controllers) ? new OpenApiColumnsBuilder($this->openapi) : null; 22 | $this->status = in_array('status', $controllers) ? new OpenApiStatusBuilder($this->openapi) : null; 23 | $this->builders = array(); 24 | foreach ($builders as $className) { 25 | $this->builders[] = new $className($this->openapi, $reflection); 26 | } 27 | } 28 | 29 | private function getServerUrl(ServerRequestInterface $request): string 30 | { 31 | $uri = $request->getUri(); 32 | $path = $uri->getPath(); 33 | $uri = $uri->withPath(trim(substr($path, 0, strpos($path, '/openapi')), '/')); 34 | return $uri->__toString(); 35 | } 36 | 37 | public function build(ServerRequestInterface $request): OpenApiDefinition 38 | { 39 | $this->openapi->set("openapi", "3.0.0"); 40 | if (!$this->openapi->has("servers")) { 41 | $this->openapi->set("servers||url", $this->getServerUrl($request)); 42 | } 43 | if ($this->records) { 44 | $this->records->build(); 45 | } 46 | if ($this->columns) { 47 | $this->columns->build(); 48 | } 49 | if ($this->status) { 50 | $this->status->build(); 51 | } 52 | foreach ($this->builders as $builder) { 53 | $builder->build(); 54 | } 55 | return $this->openapi; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php: -------------------------------------------------------------------------------- 1 | root = $base; 12 | } 13 | 14 | public function set(string $path, $value) /*: void*/ 15 | { 16 | $parts = explode('|', $path); 17 | $current = &$this->root; 18 | while (count($parts) > 0) { 19 | $part = array_shift($parts); 20 | if ($part === '') { 21 | $part = count($current); 22 | } 23 | if (!isset($current[$part])) { 24 | $current[$part] = []; 25 | } 26 | $current = &$current[$part]; 27 | } 28 | $current = $value; 29 | } 30 | 31 | public function has(string $path): bool 32 | { 33 | $parts = explode('|', trim($path, '|')); 34 | $current = &$this->root; 35 | while (count($parts) > 0) { 36 | $part = array_shift($parts); 37 | if (!isset($current[$part])) { 38 | return false; 39 | } 40 | $current = &$current[$part]; 41 | } 42 | return true; 43 | } 44 | 45 | #[\ReturnTypeWillChange] 46 | public function jsonSerialize() 47 | { 48 | return $this->root; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php: -------------------------------------------------------------------------------- 1 | builder = new OpenApiBuilder($reflection, $base, $controllers, $customBuilders); 17 | } 18 | 19 | public function get(ServerRequestInterface $request): OpenApiDefinition 20 | { 21 | return $this->builder->build(RequestFactory::fromGlobals()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/OpenApi/OpenApiStatusBuilder.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'ping' => 'get', 13 | ], 14 | ]; 15 | 16 | public function __construct(OpenApiDefinition $openapi) 17 | { 18 | $this->openapi = $openapi; 19 | } 20 | 21 | public function build() /*: void*/ 22 | { 23 | $this->setPaths(); 24 | $this->setComponentSchema(); 25 | $this->setComponentResponse(); 26 | foreach (array_keys($this->operations) as $type) { 27 | $this->setTag($type); 28 | } 29 | } 30 | 31 | private function setPaths() /*: void*/ 32 | { 33 | foreach ($this->operations as $type => $operationPair) { 34 | foreach ($operationPair as $operation => $method) { 35 | $path = "/$type/$operation"; 36 | $this->openapi->set("paths|$path|$method|tags|", "$type"); 37 | $this->openapi->set("paths|$path|$method|operationId", "$operation" . "_" . "$type"); 38 | $this->openapi->set("paths|$path|$method|description", "Request API '$operation' status"); 39 | $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-$type"); 40 | 41 | } 42 | } 43 | } 44 | 45 | private function setComponentSchema() /*: void*/ 46 | { 47 | foreach ($this->operations as $type => $operationPair) { 48 | foreach ($operationPair as $operation => $method) { 49 | $prefix = "components|schemas|$operation-$type"; 50 | $this->openapi->set("$prefix|type", "object"); 51 | switch ($operation) { 52 | case 'ping': 53 | $this->openapi->set("$prefix|required", ['db', 'cache']); 54 | $this->openapi->set("$prefix|properties|db|type", 'integer'); 55 | $this->openapi->set("$prefix|properties|db|format", "int64"); 56 | $this->openapi->set("$prefix|properties|cache|type", 'integer'); 57 | $this->openapi->set("$prefix|properties|cache|format", "int64"); 58 | break; 59 | } 60 | } 61 | } 62 | } 63 | 64 | private function setComponentResponse() /*: void*/ 65 | { 66 | foreach ($this->operations as $type => $operationPair) { 67 | foreach ($operationPair as $operation => $method) { 68 | $this->openapi->set("components|responses|$operation-$type|description", "$operation status record"); 69 | $this->openapi->set("components|responses|$operation-$type|content|application/json|schema|\$ref", "#/components/schemas/$operation-$type"); 70 | } 71 | } 72 | } 73 | 74 | private function setTag(string $type) /*: void*/ 75 | { 76 | $this->openapi->set("tags|", [ 'name' => $type, 'description' => "$type operations"]); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php: -------------------------------------------------------------------------------- 1 | isMandatory($tableName, $columnName, $params)) { 40 | $result[] = $columnName; 41 | } 42 | } else { 43 | if (!$include || $this->isMandatory($tableName, $columnName, $params)) { 44 | $result[] = $columnName; 45 | } 46 | } 47 | } 48 | return $result; 49 | } 50 | 51 | public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array 52 | { 53 | $tableName = $table->getName(); 54 | $results = $table->getColumnNames(); 55 | $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true); 56 | $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false); 57 | return $results; 58 | } 59 | 60 | public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array 61 | { 62 | $results = array(); 63 | $columnNames = $this->getNames($table, $primaryTable, $params); 64 | foreach ($columnNames as $columnName) { 65 | if (property_exists($record, $columnName)) { 66 | $results[$columnName] = $record->$columnName; 67 | } 68 | } 69 | return $results; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php: -------------------------------------------------------------------------------- 1 | conditions = [$condition1, $condition2]; 12 | } 13 | 14 | public function _and(Condition $condition): Condition 15 | { 16 | if ($condition instanceof NoCondition) { 17 | return $this; 18 | } 19 | $this->conditions[] = $condition; 20 | return $this; 21 | } 22 | 23 | public function getConditions(): array 24 | { 25 | return $this->conditions; 26 | } 27 | 28 | public static function fromArray(array $conditions): Condition 29 | { 30 | $condition = new NoCondition(); 31 | foreach ($conditions as $c) { 32 | $condition = $condition->_and($c); 33 | } 34 | return $condition; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php: -------------------------------------------------------------------------------- 1 | column = $column; 16 | $this->operator = $operator; 17 | $this->value = $value; 18 | } 19 | 20 | public function getColumn(): ReflectedColumn 21 | { 22 | return $this->column; 23 | } 24 | 25 | public function getOperator(): string 26 | { 27 | return $this->operator; 28 | } 29 | 30 | public function getValue(): string 31 | { 32 | return $this->value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/Condition.php: -------------------------------------------------------------------------------- 1 | getColumn($parts[0]); 41 | $command = $parts[1]; 42 | $negate = false; 43 | $spatial = false; 44 | if (strlen($command) > 2) { 45 | if (substr($command, 0, 1) == 'n') { 46 | $negate = true; 47 | $command = substr($command, 1); 48 | } else if (substr($command, 0, 1) == 's') { 49 | $spatial = true; 50 | $command = substr($command, 1); 51 | } 52 | } 53 | if ($spatial) { 54 | if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) { 55 | $condition = new SpatialCondition($field, $command, $parts[2]); 56 | } 57 | } else { 58 | if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) { 59 | $condition = new ColumnCondition($field, $command, $parts[2]); 60 | } 61 | } 62 | if ($negate) { 63 | $condition = $condition->_not(); 64 | } 65 | return $condition; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 12 | } 13 | 14 | public function getCondition(): Condition 15 | { 16 | return $this->condition; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php: -------------------------------------------------------------------------------- 1 | conditions = [$condition1, $condition2]; 12 | } 13 | 14 | public function _or(Condition $condition): Condition 15 | { 16 | if ($condition instanceof NoCondition) { 17 | return $this; 18 | } 19 | $this->conditions[] = $condition; 20 | return $this; 21 | } 22 | 23 | public function getConditions(): array 24 | { 25 | return $this->conditions; 26 | } 27 | 28 | public static function fromArray(array $conditions): Condition 29 | { 30 | $condition = new NoCondition(); 31 | foreach ($conditions as $c) { 32 | $condition = $condition->_or($c); 33 | } 34 | return $condition; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php: -------------------------------------------------------------------------------- 1 | errorCode = $errorCode; 16 | $this->argument = $argument; 17 | $this->details = $details; 18 | } 19 | 20 | public function getStatus(): int 21 | { 22 | return $this->errorCode->getStatus(); 23 | } 24 | 25 | public function getCode(): int 26 | { 27 | return $this->errorCode->getCode(); 28 | } 29 | 30 | public function getMessage(): string 31 | { 32 | return $this->errorCode->getMessage($this->argument); 33 | } 34 | 35 | public function serialize() 36 | { 37 | return [ 38 | 'code' => $this->getCode(), 39 | 'message' => $this->getMessage(), 40 | 'details' => $this->details, 41 | ]; 42 | } 43 | 44 | #[\ReturnTypeWillChange] 45 | public function jsonSerialize() 46 | { 47 | return array_filter($this->serialize(), function ($v) {return $v !== null;}); 48 | } 49 | 50 | public static function fromException(\Throwable $exception, bool $debug) 51 | { 52 | $document = new ErrorDocument(new ErrorCode(ErrorCode::ERROR_NOT_FOUND), $exception->getMessage(), null); 53 | if ($exception instanceof \PDOException) { 54 | if (strpos(strtolower($exception->getMessage()), 'duplicate') !== false) { 55 | $document = new ErrorDocument(new ErrorCode(ErrorCode::DUPLICATE_KEY_EXCEPTION), '', null); 56 | } elseif (strpos(strtolower($exception->getMessage()), 'unique constraint') !== false) { 57 | $document = new ErrorDocument(new ErrorCode(ErrorCode::DUPLICATE_KEY_EXCEPTION), '', null); 58 | } elseif (strpos(strtolower($exception->getMessage()), 'default value') !== false) { 59 | $document = new ErrorDocument(new ErrorCode(ErrorCode::DATA_INTEGRITY_VIOLATION), '', null); 60 | } elseif (strpos(strtolower($exception->getMessage()), 'allow nulls') !== false) { 61 | $document = new ErrorDocument(new ErrorCode(ErrorCode::DATA_INTEGRITY_VIOLATION), '', null); 62 | } elseif (strpos(strtolower($exception->getMessage()), 'constraint') !== false) { 63 | $document = new ErrorDocument(new ErrorCode(ErrorCode::DATA_INTEGRITY_VIOLATION), '', null); 64 | } else { 65 | $message = $debug ? $exception->getMessage() : 'PDOException occurred (enable debug mode)'; 66 | $document = new ErrorDocument(new ErrorCode(ErrorCode::ERROR_NOT_FOUND), $message, null); 67 | } 68 | } 69 | return $document; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php: -------------------------------------------------------------------------------- 1 | records = $records; 14 | $this->results = $results; 15 | } 16 | 17 | public function getRecords(): array 18 | { 19 | return $this->records; 20 | } 21 | 22 | public function getResults(): int 23 | { 24 | return $this->results; 25 | } 26 | 27 | public function serialize() 28 | { 29 | return [ 30 | 'records' => $this->records, 31 | 'results' => $this->results, 32 | ]; 33 | } 34 | 35 | #[\ReturnTypeWillChange] 36 | public function jsonSerialize() 37 | { 38 | return array_filter($this->serialize(), function ($v) { 39 | return $v !== -1; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/ErrorCode.php: -------------------------------------------------------------------------------- 1 | ["Success", ResponseFactory::OK], 41 | 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND], 42 | 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND], 43 | 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY], 44 | 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND], 45 | 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN], 46 | 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND], 47 | 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT], 48 | 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT], 49 | 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY], 50 | 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT], 51 | 1010 => ["Data integrity violation", ResponseFactory::CONFLICT], 52 | 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED], 53 | 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN], 54 | 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY], 55 | 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN], 56 | 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED], 57 | 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN], 58 | 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN], 59 | 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN], 60 | 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN], 61 | 1020 => ["User '%s' already exists", ResponseFactory::CONFLICT], 62 | 1021 => ["Password too short (<%d characters)", ResponseFactory::UNPROCESSABLE_ENTITY], 63 | 1022 => ["Username is empty or only whitespaces", ResponseFactory::UNPROCESSABLE_ENTITY], 64 | 1023 => ["Primary key for table '%s' not found", ResponseFactory::NOT_FOUND], 65 | 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR], 66 | ]; 67 | 68 | public function __construct(int $code) 69 | { 70 | if (!isset($this->values[$code])) { 71 | $code = 9999; 72 | } 73 | $this->code = $code; 74 | $this->message = $this->values[$code][0]; 75 | $this->status = $this->values[$code][1]; 76 | } 77 | 78 | public function getCode(): int 79 | { 80 | return $this->code; 81 | } 82 | 83 | public function getMessage(string $argument): string 84 | { 85 | return sprintf($this->message, $argument); 86 | } 87 | 88 | public function getStatus(): int 89 | { 90 | return $this->status; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/FilterInfo.php: -------------------------------------------------------------------------------- 1 | $filters) { 17 | if (substr($key, 0, 6) == 'filter') { 18 | preg_match_all('/\d+|\D+/', substr($key, 6), $matches); 19 | $path = $matches[0]; 20 | foreach ($filters as $filter) { 21 | $condition = Condition::fromString($table, $filter); 22 | if (($condition instanceof NoCondition) == false) { 23 | $conditions->put($path, $condition); 24 | } 25 | } 26 | } 27 | } 28 | return $conditions; 29 | } 30 | 31 | private function combinePathTreeOfConditions(PathTree $tree): Condition 32 | { 33 | $andConditions = $tree->getValues(); 34 | $and = AndCondition::fromArray($andConditions); 35 | $orConditions = []; 36 | foreach ($tree->getKeys() as $p) { 37 | $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p)); 38 | } 39 | $or = OrCondition::fromArray($orConditions); 40 | return $and->_and($or); 41 | } 42 | 43 | public function getCombinedConditions(ReflectedTable $table, array $params): Condition 44 | { 45 | return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/HabtmValues.php: -------------------------------------------------------------------------------- 1 | pkValues = $pkValues; 13 | $this->fkValues = $fkValues; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/OrderingInfo.php: -------------------------------------------------------------------------------- 1 | hasColumn($columnName)) { 17 | continue; 18 | } 19 | $ascending = 'ASC'; 20 | if (count($parts) > 1) { 21 | if (substr(strtoupper($parts[1]), 0, 4) == "DESC") { 22 | $ascending = 'DESC'; 23 | } 24 | } 25 | $fields[] = [$columnName, $ascending]; 26 | } 27 | } 28 | if (count($fields) == 0) { 29 | return $this->getDefaultColumnOrdering($table); 30 | } 31 | return $fields; 32 | } 33 | 34 | public function getDefaultColumnOrdering(ReflectedTable $table): array 35 | { 36 | $fields = array(); 37 | $pk = $table->getPk(); 38 | if ($pk) { 39 | $fields[] = [$pk->getName(), 'ASC']; 40 | } else { 41 | foreach ($table->getColumnNames() as $columnName) { 42 | $fields[] = [$columnName, 'ASC']; 43 | } 44 | } 45 | return $fields; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/PaginationInfo.php: -------------------------------------------------------------------------------- 1 | getPageSize($params); 18 | if (isset($params['page'])) { 19 | foreach ($params['page'] as $page) { 20 | $parts = explode(',', $page, 2); 21 | $page = intval($parts[0]) - 1; 22 | $offset = $page * $pageSize; 23 | } 24 | } 25 | return $offset; 26 | } 27 | 28 | private function getPageSize(array $params): int 29 | { 30 | $pageSize = $this->DEFAULT_PAGE_SIZE; 31 | if (isset($params['page'])) { 32 | foreach ($params['page'] as $page) { 33 | $parts = explode(',', $page, 2); 34 | if (count($parts) > 1) { 35 | $pageSize = intval($parts[1]); 36 | } 37 | } 38 | } 39 | return $pageSize; 40 | } 41 | 42 | public function getResultSize(array $params): int 43 | { 44 | $numberOfRows = -1; 45 | if (isset($params['size'])) { 46 | foreach ($params['size'] as $size) { 47 | $numberOfRows = intval($size); 48 | } 49 | } 50 | return $numberOfRows; 51 | } 52 | 53 | public function getPageLimit(array $params): int 54 | { 55 | $pageLimit = -1; 56 | if ($this->hasPage($params)) { 57 | $pageLimit = $this->getPageSize($params); 58 | } 59 | $resultSize = $this->getResultSize($params); 60 | if ($resultSize >= 0) { 61 | if ($pageLimit >= 0) { 62 | $pageLimit = min($pageLimit, $resultSize); 63 | } else { 64 | $pageLimit = $resultSize; 65 | } 66 | } 67 | return $pageLimit; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/PathTree.php: -------------------------------------------------------------------------------- 1 | newTree(); 15 | } 16 | $this->tree = &$tree; 17 | } 18 | 19 | public function newTree() 20 | { 21 | return (object) ['values' => [], 'branches' => (object) []]; 22 | } 23 | 24 | public function getKeys(): array 25 | { 26 | $branches = (array) $this->tree->branches; 27 | return array_keys($branches); 28 | } 29 | 30 | public function getValues(): array 31 | { 32 | return $this->tree->values; 33 | } 34 | 35 | public function get(string $key): PathTree 36 | { 37 | if (!isset($this->tree->branches->$key)) { 38 | return null; 39 | } 40 | return new PathTree($this->tree->branches->$key); 41 | } 42 | 43 | public function put(array $path, $value) 44 | { 45 | $tree = &$this->tree; 46 | foreach ($path as $key) { 47 | if (!isset($tree->branches->$key)) { 48 | $tree->branches->$key = $this->newTree(); 49 | } 50 | $tree = &$tree->branches->$key; 51 | } 52 | $tree->values[] = $value; 53 | } 54 | 55 | public function match(array $path): array 56 | { 57 | $star = self::WILDCARD; 58 | $tree = &$this->tree; 59 | foreach ($path as $key) { 60 | if (isset($tree->branches->$key)) { 61 | $tree = &$tree->branches->$key; 62 | } elseif (isset($tree->branches->$star)) { 63 | $tree = &$tree->branches->$star; 64 | } else { 65 | return []; 66 | } 67 | } 68 | return $tree->values; 69 | } 70 | 71 | public static function fromJson( /* object */$tree): PathTree 72 | { 73 | return new PathTree($tree); 74 | } 75 | 76 | #[\ReturnTypeWillChange] 77 | public function jsonSerialize() 78 | { 79 | return $this->tree; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/Record/RecordService.php: -------------------------------------------------------------------------------- 1 | db = $db; 22 | $this->reflection = $reflection; 23 | $this->columns = new ColumnIncluder(); 24 | $this->joiner = new RelationJoiner($reflection, $this->columns); 25 | $this->filters = new FilterInfo(); 26 | $this->ordering = new OrderingInfo(); 27 | $this->pagination = new PaginationInfo(); 28 | } 29 | 30 | private function sanitizeRecord(string $tableName, /* object */ $record, string $id) 31 | { 32 | $keyset = array_keys((array) $record); 33 | foreach ($keyset as $key) { 34 | if (!$this->reflection->getTable($tableName)->hasColumn($key)) { 35 | unset($record->$key); 36 | } 37 | } 38 | if ($id != '') { 39 | $pk = $this->reflection->getTable($tableName)->getPk(); 40 | foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) { 41 | $field = $this->reflection->getTable($tableName)->getColumn($key); 42 | if ($field->getName() == $pk->getName()) { 43 | unset($record->$key); 44 | } 45 | } 46 | } 47 | } 48 | 49 | public function hasTable(string $table): bool 50 | { 51 | return $this->reflection->hasTable($table); 52 | } 53 | 54 | public function hasPrimaryKey(string $table): bool 55 | { 56 | return $this->reflection->getTable($table)->getPk()?true:false; 57 | } 58 | 59 | public function getType(string $table): string 60 | { 61 | return $this->reflection->getType($table); 62 | } 63 | 64 | public function beginTransaction() /*: void*/ 65 | { 66 | $this->db->beginTransaction(); 67 | } 68 | 69 | public function commitTransaction() /*: void*/ 70 | { 71 | $this->db->commitTransaction(); 72 | } 73 | 74 | public function rollBackTransaction() /*: void*/ 75 | { 76 | $this->db->rollBackTransaction(); 77 | } 78 | 79 | public function create(string $tableName, /* object */ $record, array $params) /*: ?int*/ 80 | { 81 | $this->sanitizeRecord($tableName, $record, ''); 82 | $table = $this->reflection->getTable($tableName); 83 | $columnValues = $this->columns->getValues($table, true, $record, $params); 84 | return $this->db->createSingle($table, $columnValues); 85 | } 86 | 87 | public function read(string $tableName, string $id, array $params) /*: ?object*/ 88 | { 89 | $table = $this->reflection->getTable($tableName); 90 | $this->joiner->addMandatoryColumns($table, $params); 91 | $columnNames = $this->columns->getNames($table, true, $params); 92 | $record = $this->db->selectSingle($table, $columnNames, $id); 93 | if ($record == null) { 94 | return null; 95 | } 96 | $records = array($record); 97 | $this->joiner->addJoins($table, $records, $params, $this->db); 98 | return $records[0]; 99 | } 100 | 101 | public function update(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/ 102 | { 103 | $this->sanitizeRecord($tableName, $record, $id); 104 | $table = $this->reflection->getTable($tableName); 105 | $columnValues = $this->columns->getValues($table, true, $record, $params); 106 | return $this->db->updateSingle($table, $columnValues, $id); 107 | } 108 | 109 | public function delete(string $tableName, string $id, array $params) /*: ?int*/ 110 | { 111 | $table = $this->reflection->getTable($tableName); 112 | return $this->db->deleteSingle($table, $id); 113 | } 114 | 115 | public function increment(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/ 116 | { 117 | $this->sanitizeRecord($tableName, $record, $id); 118 | $table = $this->reflection->getTable($tableName); 119 | $columnValues = $this->columns->getValues($table, true, $record, $params); 120 | return $this->db->incrementSingle($table, $columnValues, $id); 121 | } 122 | 123 | public function _list(string $tableName, array $params): ListDocument 124 | { 125 | $table = $this->reflection->getTable($tableName); 126 | $this->joiner->addMandatoryColumns($table, $params); 127 | $columnNames = $this->columns->getNames($table, true, $params); 128 | $condition = $this->filters->getCombinedConditions($table, $params); 129 | $columnOrdering = $this->ordering->getColumnOrdering($table, $params); 130 | if (!$this->pagination->hasPage($params)) { 131 | $offset = 0; 132 | $limit = $this->pagination->getPageLimit($params); 133 | $count = -1; 134 | } else { 135 | $offset = $this->pagination->getPageOffset($params); 136 | $limit = $this->pagination->getPageLimit($params); 137 | $count = $this->db->selectCount($table, $condition); 138 | } 139 | $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit); 140 | $this->joiner->addJoins($table, $records, $params, $this->db); 141 | return new ListDocument($records, $count); 142 | } 143 | 144 | public function ping(): int 145 | { 146 | return $this->db->ping(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/RequestFactory.php: -------------------------------------------------------------------------------- 1 | fromGlobals(); 16 | $stream = $psr17Factory->createStreamFromFile('php://input'); 17 | $serverRequest = $serverRequest->withBody($stream); 18 | return $serverRequest; 19 | } 20 | 21 | public static function fromString(string $request): ServerRequestInterface 22 | { 23 | $parts = explode("\n\n", trim($request), 2); 24 | $lines = explode("\n", $parts[0]); 25 | $first = explode(' ', trim(array_shift($lines)), 2); 26 | $method = $first[0]; 27 | $body = isset($parts[1]) ? $parts[1] : ''; 28 | $url = isset($first[1]) ? $first[1] : ''; 29 | 30 | $psr17Factory = new Psr17Factory(); 31 | $serverRequest = $psr17Factory->createServerRequest($method, $url); 32 | foreach ($lines as $line) { 33 | list($key, $value) = explode(':', $line, 2); 34 | $serverRequest = $serverRequest->withAddedHeader($key, $value); 35 | } 36 | if ($body) { 37 | $stream = $psr17Factory->createStream($body); 38 | $stream->rewind(); 39 | $serverRequest = $serverRequest->withBody($stream); 40 | } 41 | return $serverRequest; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/RequestUtils.php: -------------------------------------------------------------------------------- 1 | withUri($request->getUri()->withQuery($query)); 14 | } 15 | 16 | public static function getHeader(ServerRequestInterface $request, string $header): string 17 | { 18 | $headers = $request->getHeader($header); 19 | return isset($headers[0]) ? $headers[0] : ''; 20 | } 21 | 22 | public static function getParams(ServerRequestInterface $request): array 23 | { 24 | $params = array(); 25 | $query = $request->getUri()->getQuery(); 26 | //$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query)); 27 | $query = str_replace('%5D%5B%5D=', '%5D=', str_replace('=', '%5B%5D=', $query)); 28 | parse_str($query, $params); 29 | return $params; 30 | } 31 | 32 | public static function getPathSegment(ServerRequestInterface $request, int $part): string 33 | { 34 | $path = $request->getUri()->getPath(); 35 | $pathSegments = explode('/', rtrim($path, '/')); 36 | if ($part < 0 || $part >= count($pathSegments)) { 37 | return ''; 38 | } 39 | return urldecode($pathSegments[$part]); 40 | } 41 | 42 | public static function getOperation(ServerRequestInterface $request): string 43 | { 44 | $method = $request->getMethod(); 45 | $path = RequestUtils::getPathSegment($request, 1); 46 | $hasPk = RequestUtils::getPathSegment($request, 3) != ''; 47 | switch ($path) { 48 | case 'openapi': 49 | return 'document'; 50 | case 'columns': 51 | return $method == 'get' ? 'reflect' : 'remodel'; 52 | case 'geojson': 53 | case 'records': 54 | switch ($method) { 55 | case 'POST': 56 | return 'create'; 57 | case 'GET': 58 | return $hasPk ? 'read' : 'list'; 59 | case 'PUT': 60 | return 'update'; 61 | case 'DELETE': 62 | return 'delete'; 63 | case 'PATCH': 64 | return 'increment'; 65 | } 66 | } 67 | return 'unknown'; 68 | } 69 | 70 | private static function getJoinTables(string $tableName, array $parameters): array 71 | { 72 | $uniqueTableNames = array(); 73 | $uniqueTableNames[$tableName] = true; 74 | if (isset($parameters['join'])) { 75 | foreach ($parameters['join'] as $parameter) { 76 | $tableNames = explode(',', trim($parameter)); 77 | foreach ($tableNames as $tableName) { 78 | $uniqueTableNames[$tableName] = true; 79 | } 80 | } 81 | } 82 | return array_keys($uniqueTableNames); 83 | } 84 | 85 | public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array 86 | { 87 | $path = RequestUtils::getPathSegment($request, 1); 88 | $tableName = RequestUtils::getPathSegment($request, 2); 89 | $allTableNames = $reflection->getTableNames(); 90 | switch ($path) { 91 | case 'openapi': 92 | return $allTableNames; 93 | case 'columns': 94 | return $tableName ? [$tableName] : $allTableNames; 95 | case 'records': 96 | return self::getJoinTables($tableName, RequestUtils::getParams($request)); 97 | } 98 | return $allTableNames; 99 | } 100 | 101 | public static function toString(ServerRequestInterface $request): string 102 | { 103 | $method = $request->getMethod(); 104 | $uri = $request->getUri()->__toString(); 105 | $headers = $request->getHeaders(); 106 | $request->getBody()->rewind(); 107 | $body = $request->getBody()->getContents(); 108 | 109 | $str = "$method $uri\n"; 110 | foreach ($headers as $key => $values) { 111 | foreach ($values as $value) { 112 | $str .= "$key: $value\n"; 113 | } 114 | } 115 | if ($body !== '') { 116 | $str .= "\n"; 117 | $str .= "$body\n"; 118 | } 119 | return $str; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | createResponse($status); 47 | $stream = $psr17Factory->createStream($content); 48 | $stream->rewind(); 49 | $response = $response->withBody($stream); 50 | $response = $response->withHeader('Content-Type', $contentType . '; charset=utf-8'); 51 | $response = $response->withHeader('Content-Length', strlen($content)); 52 | return $response; 53 | } 54 | 55 | public static function fromStatus(int $status): ResponseInterface 56 | { 57 | $psr17Factory = new Psr17Factory(); 58 | return $psr17Factory->createResponse($status); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Tqdev/PhpCrudApi/ResponseUtils.php: -------------------------------------------------------------------------------- 1 | getStatusCode(); 12 | $headers = $response->getHeaders(); 13 | $body = $response->getBody()->getContents(); 14 | 15 | http_response_code($status); 16 | foreach ($headers as $key => $values) { 17 | foreach ($values as $value) { 18 | header("$key: $value"); 19 | } 20 | } 21 | echo $body; 22 | } 23 | 24 | public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface 25 | { 26 | $response = $response->withHeader('X-Exception-Name', get_class($e)); 27 | $response = $response->withHeader('X-Exception-Message', preg_replace('|\n|', ' ', trim($e->getMessage()))); 28 | $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine()); 29 | return $response; 30 | } 31 | 32 | public static function toString(ResponseInterface $response): string 33 | { 34 | $status = $response->getStatusCode(); 35 | $headers = $response->getHeaders(); 36 | $response->getBody()->rewind(); 37 | $body = $response->getBody()->getContents(); 38 | 39 | $str = "$status\n"; 40 | foreach ($headers as $key => $values) { 41 | foreach ($values as $value) { 42 | $str .= "$key: $value\n"; 43 | } 44 | } 45 | if ($body !== '') { 46 | $str .= "\n"; 47 | $str .= "$body\n"; 48 | } 49 | return $str; 50 | } 51 | } 52 | --------------------------------------------------------------------------------