Skip to content

Commit 87d01fc

Browse files
committed
Merge branch 'master' of github.com:mevdschee/php-crud-api
2 parents 9130f25 + ecf35b9 commit 87d01fc

File tree

8 files changed

+131
-70
lines changed

8 files changed

+131
-70
lines changed

README.md

+23-4
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,21 @@ These are all the configuration options and their default value between brackets
8080
- "cacheType": `TempFile`, `Redis`, `Memcache`, `Memcached` or `NoCache` (`TempFile`)
8181
- "cachePath": Path/address of the cache (defaults to system's temp directory)
8282
- "cacheTime": Number of seconds the cache is valid (`10`)
83-
- "debug": Show errors in the "X-Debug-Info" header (`false`)
83+
- "debug": Show errors in the "X-Exception" headers (`false`)
8484
- "basePath": URI base path of the API (determined using PATH_INFO by default)
8585

86+
All configuration options are also available as environment variables. Write the config option with capitals, a "PHP_CRUD_API_" prefix and underscores for word breakes, so for instance:
87+
88+
- PHP_CRUD_API_DRIVER=mysql
89+
- PHP_CRUD_API_ADDRESS=localhost
90+
- PHP_CRUD_API_PORT=3306
91+
- PHP_CRUD_API_DATABASE=php-crud-api
92+
- PHP_CRUD_API_USERNAME=php-crud-api
93+
- PHP_CRUD_API_PASSWORD=php-crud-api
94+
- PHP_CRUD_API_DEBUG=1
95+
96+
The environment variables take precedence over the PHP configuration.
97+
8698
## Limitations
8799

88100
These limitation and constrains apply:
@@ -614,10 +626,10 @@ You can tune the middleware behavior using middleware specific configuration par
614626
- "firewall.reverseProxy": Set to "true" when a reverse proxy is used ("")
615627
- "firewall.allowedIpAddresses": List of IP addresses that are allowed to connect ("")
616628
- "cors.allowedOrigins": The origins allowed in the CORS headers ("*")
617-
- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN, X-Authorization, X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File")
629+
- "cors.allowHeaders": The headers allowed in the CORS request ("Content-Type, X-XSRF-TOKEN, X-Authorization")
618630
- "cors.allowMethods": The methods allowed in the CORS request ("OPTIONS, GET, PUT, POST, DELETE, PATCH")
619631
- "cors.allowCredentials": To allow credentials in the CORS request ("true")
620-
- "cors.exposeHeaders": Whitelist headers that browsers are allowed to access ("X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File")
632+
- "cors.exposeHeaders": Whitelist headers that browsers are allowed to access ("")
621633
- "cors.maxAge": The time that the CORS grant is valid in seconds ("1728000")
622634
- "xsrf.excludeMethods": The methods that do not require XSRF protection ("OPTIONS,GET")
623635
- "xsrf.cookieName": The name of the XSRF protection cookie ("XSRF-TOKEN")
@@ -653,6 +665,7 @@ You can tune the middleware behavior using middleware specific configuration par
653665
- "reconnect.passwordHandler": Handler to implement retrieval of the database password ("")
654666
- "authorization.tableHandler": Handler to implement table authorization rules ("")
655667
- "authorization.columnHandler": Handler to implement column authorization rules ("")
668+
- "authorization.pathHandler": Handler to implement path authorization rules ("")
656669
- "authorization.recordHandler": Handler to implement record authorization filter rules ("")
657670
- "validation.handler": Handler to implement validation rules for input values ("")
658671
- "validation.types": Types to enable type validation for, empty means 'none' ("all")
@@ -840,7 +853,7 @@ Add the "columns" controller in the configuration to enable this functionality.
840853

841854
### Authorizing tables, columns and records
842855

843-
By default all tables and columns are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
856+
By default all tables, columns and paths are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
844857
and define a 'authorization.tableHandler' function that returns 'false' for these tables.
845858

846859
'authorization.tableHandler' => function ($operation, $tableName) {
@@ -862,6 +875,12 @@ The above example will restrict access to the 'password' field of the 'users' ta
862875
The above example will disallow access to user records where the username is 'admin'.
863876
This construct adds a filter to every executed query.
864877

878+
'authorization.pathHandler' => function ($path) {
879+
return $path === 'openapi' ? false : true;
880+
},
881+
882+
The above example will disabled the `/openapi` route.
883+
865884
NB: You need to handle the creation of invalid records with a validation (or sanitation) handler.
866885

867886
### SQL GRANT authorization

api.php

+54-33
Original file line numberDiff line numberDiff line change
@@ -7056,6 +7056,7 @@ public function route(ServerRequestInterface $request): ResponseInterface
70567056
$data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
70577057
$this->cache->set('PathTree', $data, $this->ttl);
70587058
}
7059+
70597060
return $this->handle($request);
70607061
}
70617062

@@ -7164,6 +7165,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
71647165
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
71657166
use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
71667167
use Tqdev\PhpCrudApi\Middleware\Router\Router;
7168+
use Tqdev\PhpCrudApi\Record\ErrorCode;
71677169
use Tqdev\PhpCrudApi\Record\FilterInfo;
71687170
use Tqdev\PhpCrudApi\RequestUtils;
71697171

@@ -7225,9 +7227,20 @@ private function handleRecords(string $operation, string $tableName) /*: void*/
72257227
}
72267228
}
72277229

7230+
private function pathHandler(string $path) /*: bool*/
7231+
{
7232+
$pathHandler = $this->getProperty('pathHandler', '');
7233+
return $pathHandler ? call_user_func($pathHandler, $path) : true;
7234+
}
7235+
72287236
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
72297237
{
72307238
$path = RequestUtils::getPathSegment($request, 1);
7239+
7240+
if (!$this->pathHandler($path)) {
7241+
return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
7242+
}
7243+
72317244
$operation = RequestUtils::getOperation($request);
72327245
$tableNames = RequestUtils::getTableNames($request, $this->reflection);
72337246
foreach ($tableNames as $tableName) {
@@ -7370,12 +7383,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
73707383
use Psr\Http\Message\ResponseInterface;
73717384
use Psr\Http\Message\ServerRequestInterface;
73727385
use Psr\Http\Server\RequestHandlerInterface;
7386+
use Tqdev\PhpCrudApi\Controller\Responder;
73737387
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
7388+
use Tqdev\PhpCrudApi\Middleware\Router\Router;
73747389
use Tqdev\PhpCrudApi\Record\ErrorCode;
73757390
use Tqdev\PhpCrudApi\ResponseFactory;
7391+
use Tqdev\PhpCrudApi\ResponseUtils;
73767392

73777393
class CorsMiddleware extends Middleware
73787394
{
7395+
private $debug;
7396+
7397+
public function __construct(Router $router, Responder $responder, array $properties, bool $debug)
7398+
{
7399+
parent::__construct($router, $responder, $properties);
7400+
$this->debug = $debug;
7401+
}
7402+
73797403
private function isOriginAllowed(string $origin, string $allowedOrigins): bool
73807404
{
73817405
$found = false;
@@ -7399,7 +7423,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
73997423
$response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
74007424
} elseif ($method == 'OPTIONS') {
74017425
$response = ResponseFactory::fromStatus(ResponseFactory::OK);
7402-
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization, X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File');
7426+
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
7427+
if ($this->debug) {
7428+
$allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
7429+
}
74037430
if ($allowHeaders) {
74047431
$response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
74057432
}
@@ -7415,12 +7442,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
74157442
if ($maxAge) {
74167443
$response = $response->withHeader('Access-Control-Max-Age', $maxAge);
74177444
}
7418-
$exposeHeaders = $this->getProperty('exposeHeaders', 'X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File');
7445+
$exposeHeaders = $this->getProperty('exposeHeaders', '');
7446+
if ($this->debug) {
7447+
$exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
7448+
}
74197449
if ($exposeHeaders) {
74207450
$response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
74217451
}
74227452
} else {
7423-
$response = $next->handle($request);
7453+
$response = null;
7454+
try {
7455+
$response = $next->handle($request);
7456+
} catch (\Throwable $e) {
7457+
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
7458+
if ($this->debug) {
7459+
$response = ResponseUtils::addExceptionHeaders($response, $e);
7460+
}
7461+
}
74247462
}
74257463
if ($origin) {
74267464
$allowCredentials = $this->getProperty('allowCredentials', 'true');
@@ -9937,27 +9975,19 @@ public function getStatus(): int
99379975

99389976
class FilterInfo
99399977
{
9940-
private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
9941-
{
9942-
$key = 'filter' . implode('', $path);
9943-
if (isset($params[$key])) {
9944-
foreach ($params[$key] as $filter) {
9945-
$condition = Condition::fromString($table, $filter);
9946-
if (($condition instanceof NoCondition) == false) {
9947-
$conditions->put($path, $condition);
9948-
}
9949-
}
9950-
}
9951-
}
9952-
99539978
private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
99549979
{
99559980
$conditions = new PathTree();
9956-
$this->addConditionFromFilterPath($conditions, [], $table, $params);
9957-
for ($n = ord('0'); $n <= ord('9'); $n++) {
9958-
$this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
9959-
for ($l = ord('a'); $l <= ord('f'); $l++) {
9960-
$this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
9981+
foreach ($params as $key => $filters) {
9982+
if (substr($key, 0, 6) == 'filter') {
9983+
preg_match_all('/\d+|\D+/', substr($key, 6), $matches);
9984+
$path = $matches[0];
9985+
foreach ($filters as $filter) {
9986+
$condition = Condition::fromString($table, $filter);
9987+
if (($condition instanceof NoCondition) == false) {
9988+
$conditions->put($path, $condition);
9989+
}
9990+
}
99619991
}
99629992
}
99639993
return $conditions;
@@ -10686,7 +10716,7 @@ public function __construct(Config $config)
1068610716
new SslRedirectMiddleware($router, $responder, $properties);
1068710717
break;
1068810718
case 'cors':
10689-
new CorsMiddleware($router, $responder, $properties);
10719+
new CorsMiddleware($router, $responder, $properties, $config->getDebug());
1069010720
break;
1069110721
case 'firewall':
1069210722
new FirewallMiddleware($router, $responder, $properties);
@@ -10829,16 +10859,7 @@ private function applyParsedBodyHack(ServerRequestInterface $request): ServerReq
1082910859

1083010860
public function handle(ServerRequestInterface $request): ResponseInterface
1083110861
{
10832-
$response = null;
10833-
try {
10834-
$response = $this->router->route($this->addParsedBody($request));
10835-
} catch (\Throwable $e) {
10836-
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
10837-
if ($this->debug) {
10838-
$response = ResponseUtils::addExceptionHeaders($response, $e);
10839-
}
10840-
}
10841-
return $response;
10862+
return $this->router->route($this->addParsedBody($request));
1084210863
}
1084310864
}
1084410865
}
@@ -10856,7 +10877,7 @@ class Config
1085610877
'password' => null,
1085710878
'database' => null,
1085810879
'tables' => '',
10859-
'middlewares' => 'cors',
10880+
'middlewares' => 'cors,errors',
1086010881
'controllers' => 'records,geojson,openapi',
1086110882
'customControllers' => '',
1086210883
'customOpenApiBuilders' => '',

src/Tqdev/PhpCrudApi/Api.php

+2-11
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function __construct(Config $config)
6767
new SslRedirectMiddleware($router, $responder, $properties);
6868
break;
6969
case 'cors':
70-
new CorsMiddleware($router, $responder, $properties);
70+
new CorsMiddleware($router, $responder, $properties, $config->getDebug());
7171
break;
7272
case 'firewall':
7373
new FirewallMiddleware($router, $responder, $properties);
@@ -210,15 +210,6 @@ private function applyParsedBodyHack(ServerRequestInterface $request): ServerReq
210210

211211
public function handle(ServerRequestInterface $request): ResponseInterface
212212
{
213-
$response = null;
214-
try {
215-
$response = $this->router->route($this->addParsedBody($request));
216-
} catch (\Throwable $e) {
217-
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
218-
if ($this->debug) {
219-
$response = ResponseUtils::addExceptionHeaders($response, $e);
220-
}
221-
}
222-
return $response;
213+
return $this->router->route($this->addParsedBody($request));
223214
}
224215
}

src/Tqdev/PhpCrudApi/Config.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Config
1212
'password' => null,
1313
'database' => null,
1414
'tables' => '',
15-
'middlewares' => 'cors',
15+
'middlewares' => 'cors,errors',
1616
'controllers' => 'records,geojson,openapi',
1717
'customControllers' => '',
1818
'customOpenApiBuilders' => '',

src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php

+12
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
1111
use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
1212
use Tqdev\PhpCrudApi\Middleware\Router\Router;
13+
use Tqdev\PhpCrudApi\Record\ErrorCode;
1314
use Tqdev\PhpCrudApi\Record\FilterInfo;
1415
use Tqdev\PhpCrudApi\RequestUtils;
1516

@@ -71,9 +72,20 @@ private function handleRecords(string $operation, string $tableName) /*: void*/
7172
}
7273
}
7374

75+
private function pathHandler(string $path) /*: bool*/
76+
{
77+
$pathHandler = $this->getProperty('pathHandler', '');
78+
return $pathHandler ? call_user_func($pathHandler, $path) : true;
79+
}
80+
7481
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
7582
{
7683
$path = RequestUtils::getPathSegment($request, 1);
84+
85+
if (!$this->pathHandler($path)) {
86+
return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
87+
}
88+
7789
$operation = RequestUtils::getOperation($request);
7890
$tableNames = RequestUtils::getTableNames($request, $this->reflection);
7991
foreach ($tableNames as $tableName) {

src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php

+28-3
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@
55
use Psr\Http\Message\ResponseInterface;
66
use Psr\Http\Message\ServerRequestInterface;
77
use Psr\Http\Server\RequestHandlerInterface;
8+
use Tqdev\PhpCrudApi\Controller\Responder;
89
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
10+
use Tqdev\PhpCrudApi\Middleware\Router\Router;
911
use Tqdev\PhpCrudApi\Record\ErrorCode;
1012
use Tqdev\PhpCrudApi\ResponseFactory;
13+
use Tqdev\PhpCrudApi\ResponseUtils;
1114

1215
class CorsMiddleware extends Middleware
1316
{
17+
private $debug;
18+
19+
public function __construct(Router $router, Responder $responder, array $properties, bool $debug)
20+
{
21+
parent::__construct($router, $responder, $properties);
22+
$this->debug = $debug;
23+
}
24+
1425
private function isOriginAllowed(string $origin, string $allowedOrigins): bool
1526
{
1627
$found = false;
@@ -34,7 +45,10 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
3445
$response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
3546
} elseif ($method == 'OPTIONS') {
3647
$response = ResponseFactory::fromStatus(ResponseFactory::OK);
37-
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization, X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File');
48+
$allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
49+
if ($this->debug) {
50+
$allowHeaders = implode(', ', array_filter([$allowHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
51+
}
3852
if ($allowHeaders) {
3953
$response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
4054
}
@@ -50,12 +64,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
5064
if ($maxAge) {
5165
$response = $response->withHeader('Access-Control-Max-Age', $maxAge);
5266
}
53-
$exposeHeaders = $this->getProperty('exposeHeaders', 'X-Debug-Info, X-Exception-Name, X-Exception-Message, X-Exception-File');
67+
$exposeHeaders = $this->getProperty('exposeHeaders', '');
68+
if ($this->debug) {
69+
$exposeHeaders = implode(', ', array_filter([$exposeHeaders, 'X-Exception-Name, X-Exception-Message, X-Exception-File']));
70+
}
5471
if ($exposeHeaders) {
5572
$response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
5673
}
5774
} else {
58-
$response = $next->handle($request);
75+
$response = null;
76+
try {
77+
$response = $next->handle($request);
78+
} catch (\Throwable $e) {
79+
$response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
80+
if ($this->debug) {
81+
$response = ResponseUtils::addExceptionHeaders($response, $e);
82+
}
83+
}
5984
}
6085
if ($origin) {
6186
$allowCredentials = $this->getProperty('allowCredentials', 'true');

src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public function route(ServerRequestInterface $request): ResponseInterface
9595
$data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
9696
$this->cache->set('PathTree', $data, $this->ttl);
9797
}
98+
9899
return $this->handle($request);
99100
}
100101

0 commit comments

Comments
 (0)