Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[php-slim4] Add lazy CORS implementation #11941

Merged
merged 2 commits into from Mar 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -225,6 +225,7 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("register_dependencies.mustache", toSrcPath(appPackage, srcBasePath), "RegisterDependencies.php"));
supportingFiles.add(new SupportingFile("register_middlewares.mustache", toSrcPath(appPackage, srcBasePath), "RegisterMiddlewares.php"));
supportingFiles.add(new SupportingFile("register_routes.mustache", toSrcPath(appPackage, srcBasePath), "RegisterRoutes.php"));
supportingFiles.add(new SupportingFile("response_emitter.mustache", toSrcPath(appPackage, srcBasePath), "ResponseEmitter.php"));

// don't generate phpunit config when tests generation disabled
if (Boolean.TRUE.equals(generateApiTests) || Boolean.TRUE.equals(generateModelTests)) {
Expand Down
Expand Up @@ -17,6 +17,7 @@
{{#isZendDiactoros}}
"laminas/laminas-diactoros": "^2.3.0",
{{/isZendDiactoros}}
"neomerx/cors-psr7": "^2.0",
{{#isNyholmPsr7}}
"nyholm/psr7": "^1.3.0",
"nyholm/psr7-server": "^0.4.1",
Expand Down
Expand Up @@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => false,

// CORS settings
// @see https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
false, // isPreFlightCanBeCached
0, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
true, // areAllOriginsAllowed
[], // allowedOrigins
true, // areAllMethodsAllowed
[], // allowedLcMethods
'GET, POST, PUT, PATCH, DELETE', // allowedMethodsList
true, // areAllHeadersAllowed
[], // allowedLcHeaders
'authorization, content-type, x-requested-with', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],

// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',
Expand Down
Expand Up @@ -32,6 +32,29 @@ return [
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => true,

// CORS settings
// https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
true, // isPreFlightCanBeCached
86400, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
false, // areAllOriginsAllowed
[], // allowedOrigins
false, // areAllMethodsAllowed
[], // allowedLcMethods
'', // allowedMethodsList
false, // areAllHeadersAllowed
[], // allowedLcHeaders
'', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],

// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',
Expand Down
Expand Up @@ -14,8 +14,10 @@ use DI\ContainerBuilder;
use {{appPackage}}\RegisterDependencies;
use {{appPackage}}\RegisterRoutes;
use {{appPackage}}\RegisterMiddlewares;
use {{appPackage}}\ResponseEmitter;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Slim\Factory\ServerRequestCreatorFactory;
use Slim\ResponseEmitter;
use Slim\Middleware\ErrorMiddleware;
{{/apiInfo}}

// Instantiate PHP-DI ContainerBuilder
Expand Down Expand Up @@ -66,7 +68,15 @@ $routes($app);
$serverRequestCreator = ServerRequestCreatorFactory::create();
$request = $serverRequestCreator->createServerRequestFromGlobals();

// Get error middleware from container
// also anti-pattern, of course we know
$errorMiddleware = $container->get(ErrorMiddleware::class);

// Run App & Emit Response
$response = $app->handle($request);
$responseEmitter = new ResponseEmitter();
$responseEmitter = (new ResponseEmitter())
->setRequest($request)
->setErrorMiddleware($errorMiddleware)
->setAnalyzer($container->get(AnalyzerInterface::class));

$responseEmitter->emit($response);
Expand Up @@ -45,6 +45,12 @@ final class RegisterDependencies
->constructorParameter('logErrors', \DI\get('slim.logErrors', true))
->constructorParameter('logErrorDetails', \DI\get('slim.logErrorDetails', true)),

// CORS
\Neomerx\Cors\Contracts\AnalysisStrategyInterface::class => \DI\create(\Neomerx\Cors\Strategies\Settings::class)
->method('setData', \DI\get('cors.settings')),

\Neomerx\Cors\Contracts\AnalyzerInterface::class => \DI\factory([\Neomerx\Cors\Analyzer::class, 'instance']),

// PDO class for database managing
\PDO::class => \DI\create()
->constructor(
Expand Down
Expand Up @@ -11,6 +11,8 @@ declare(strict_types=1);
*/{{#apiInfo}}
namespace {{appPackage}};

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpNotImplementedException;

/**
Expand Down Expand Up @@ -109,8 +111,13 @@ class RegisterRoutes
*/
public function __invoke(\Slim\App $app): void
{
$app->options('/{routes:.*}', function (ServerRequestInterface $request, ResponseInterface $response) {
// CORS Pre-Flight OPTIONS Request Handler
return $response;
});

foreach ($this->operations as $operation) {
$callback = function ($request) use ($operation) {
$callback = function (ServerRequestInterface $request) use ($operation) {
$message = "How about extending {$operation['classname']} by {$operation['apiPackage']}\\{$operation['userClassname']} class implementing {$operation['operationId']} as a {$operation['httpMethod']} method?";
throw new HttpNotImplementedException($request, $message);
};
Expand Down
@@ -0,0 +1,130 @@
<?php

{{>licenseInfo}}

declare(strict_types=1);

/**
* NOTE: This class is auto generated by the openapi generator program.
* https://github.com/openapitools/openapi-generator
* Do not edit the class manually.
*/{{#apiInfo}}
namespace {{appPackage}};
{{/apiInfo}}

use Neomerx\Cors\Contracts\AnalysisResultInterface;
use Neomerx\Cors\Contracts\AnalyzerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\RequestInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Middleware\ErrorMiddleware;
use Slim\ResponseEmitter as SlimResponseEmitter;

/**
* Custom response emitter to support lazy CORS.
* @see https://github.com/slimphp/Slim-Skeleton/blob/037cfa2b6885301fc32a5b18a00a251a534aac81/src/Application/ResponseEmitter/ResponseEmitter.php
*/
class ResponseEmitter extends SlimResponseEmitter
{
/**
* @var RequestInterface
*/
protected $request;

/**
* @var ErrorMiddleware
*/
protected $errorMiddleware;

/**
* @var AnalyzerInterface
*/
protected $analyzer;

/**
* Set request.
* @param RequestInterface $request
* @return ResponseEmitter
*/
public function setRequest(RequestInterface $request): ResponseEmitter
{
$this->request = $request;
return $this;
}

/**
* Set error middleware.
* @param ErrorMiddleware $errorMiddleware
* @return ResponseEmitter
*/
public function setErrorMiddleware(ErrorMiddleware $errorMiddleware): ResponseEmitter
{
$this->errorMiddleware = $errorMiddleware;
return $this;
}

/**
* Set CORS request analyzer.
* @param AnalyzerInterface $analyzer
* @return ResponseEmitter
*/
public function setAnalyzer(AnalyzerInterface $analyzer): ResponseEmitter
{
$this->analyzer = $analyzer;
return $this;
}

/**
* Send the response the client
*
* @param ResponseInterface $response
* @return void
*/
public function emit(ResponseInterface $response): void
{
// slightly modified CORS handler from package documentation example
// @see https://github.com/neomerx/cors-psr7#sample-usage
$cors = $this->analyzer->analyze($this->request);
$errorMsg = null;

switch ($cors->getRequestType()) {
case AnalysisResultInterface::ERR_NO_HOST_HEADER:
$errorMsg = 'CORS no host header in request';
break;
case AnalysisResultInterface::ERR_ORIGIN_NOT_ALLOWED:
$errorMsg = 'CORS request origin is not allowed';
break;
case AnalysisResultInterface::ERR_METHOD_NOT_SUPPORTED:
$errorMsg = 'CORS requested method is not supported';
break;
case AnalysisResultInterface::ERR_HEADERS_NOT_SUPPORTED:
$errorMsg = 'CORS requested header is not allowed';
break;
case AnalysisResultInterface::TYPE_REQUEST_OUT_OF_CORS_SCOPE:
// do nothing
break;
case AnalysisResultInterface::TYPE_PRE_FLIGHT_REQUEST:
default:
// actual CORS request
$corsHeaders = $cors->getResponseHeaders();

// add CORS headers to Response $response
foreach ($corsHeaders as $name => $value) {
$response = $response->withHeader($name, $value);
}
}

if (!empty($errorMsg)) {
$exception = new HttpBadRequestException($this->request, sprintf('%s.', $errorMsg));
$exception->setTitle(\sprintf('%d %s', $exception->getCode(), $errorMsg));
$exception->setDescription($exception->getMessage());
$response = $this->errorMiddleware->handleException($this->request, $exception);
}

if (ob_get_contents()) {
ob_clean();
}

parent::emit($response);
}
}
1 change: 1 addition & 0 deletions samples/server/petstore/php-slim4/.openapi-generator/FILES
Expand Up @@ -11,6 +11,7 @@ lib/Api/AbstractUserApi.php
lib/App/RegisterDependencies.php
lib/App/RegisterMiddlewares.php
lib/App/RegisterRoutes.php
lib/App/ResponseEmitter.php
lib/Auth/AbstractAuthenticator.php
lib/BaseModel.php
lib/Model/ApiResponse.php
Expand Down
1 change: 1 addition & 0 deletions samples/server/petstore/php-slim4/composer.json
Expand Up @@ -10,6 +10,7 @@
"require": {
"php": "^7.4 || ^8.0",
"dyorg/slim-token-authentication": "dev-slim4",
"neomerx/cors-psr7": "^2.0",
"php-di/slim-bridge": "^3.2",
"slim/psr7": "^1.1.0",
"ybelenko/openapi-data-mocker": "^1.0",
Expand Down
23 changes: 23 additions & 0 deletions samples/server/petstore/php-slim4/config/dev/default.inc.php
Expand Up @@ -32,6 +32,29 @@
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => false,

// CORS settings
// @see https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
false, // isPreFlightCanBeCached
0, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
true, // areAllOriginsAllowed
[], // allowedOrigins
true, // areAllMethodsAllowed
[], // allowedLcMethods
'GET, POST, PUT, PATCH, DELETE', // allowedMethodsList
true, // areAllHeadersAllowed
[], // allowedLcHeaders
'authorization, content-type, x-requested-with', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],

// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',
Expand Down
23 changes: 23 additions & 0 deletions samples/server/petstore/php-slim4/config/prod/default.inc.php
Expand Up @@ -32,6 +32,29 @@
// Doesn't do anything when 'logErrors' is false.
'slim.logErrorDetails' => true,

// CORS settings
// https://github.com/neomerx/cors-psr7/blob/master/src/Strategies/Settings.php
'cors.settings' => [
isset($_SERVER['HTTPS']) ? 'https' : 'http', // serverOriginScheme
$_SERVER['SERVER_NAME'], // serverOriginHost
null, // serverOriginPort
true, // isPreFlightCanBeCached
86400, // preFlightCacheMaxAge
false, // isForceAddMethods
false, // isForceAddHeaders
true, // isUseCredentials
false, // areAllOriginsAllowed
[], // allowedOrigins
false, // areAllMethodsAllowed
[], // allowedLcMethods
'', // allowedMethodsList
false, // areAllHeadersAllowed
[], // allowedLcHeaders
'', // allowedHeadersList
'', // exposedHeadersList
true, // isCheckHost
],

// PDO
'pdo.dsn' => 'mysql:host=localhost;charset=utf8mb4',
'pdo.username' => 'root',
Expand Down
Expand Up @@ -58,6 +58,12 @@ public function __invoke(\DI\ContainerBuilder $containerBuilder): void
->constructorParameter('logErrors', \DI\get('slim.logErrors', true))
->constructorParameter('logErrorDetails', \DI\get('slim.logErrorDetails', true)),

// CORS
\Neomerx\Cors\Contracts\AnalysisStrategyInterface::class => \DI\create(\Neomerx\Cors\Strategies\Settings::class)
->method('setData', \DI\get('cors.settings')),

\Neomerx\Cors\Contracts\AnalyzerInterface::class => \DI\factory([\Neomerx\Cors\Analyzer::class, 'instance']),

// PDO class for database managing
\PDO::class => \DI\create()
->constructor(
Expand Down
9 changes: 8 additions & 1 deletion samples/server/petstore/php-slim4/lib/App/RegisterRoutes.php
Expand Up @@ -24,6 +24,8 @@
*/
namespace OpenAPIServer\App;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Exception\HttpNotImplementedException;

/**
Expand Down Expand Up @@ -838,8 +840,13 @@ class RegisterRoutes
*/
public function __invoke(\Slim\App $app): void
{
$app->options('/{routes:.*}', function (ServerRequestInterface $request, ResponseInterface $response) {
// CORS Pre-Flight OPTIONS Request Handler
return $response;
});

foreach ($this->operations as $operation) {
$callback = function ($request) use ($operation) {
$callback = function (ServerRequestInterface $request) use ($operation) {
$message = "How about extending {$operation['classname']} by {$operation['apiPackage']}\\{$operation['userClassname']} class implementing {$operation['operationId']} as a {$operation['httpMethod']} method?";
throw new HttpNotImplementedException($request, $message);
};
Expand Down