diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java index 64a94cecf622..52e7f4ec507b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java @@ -225,6 +225,7 @@ public class PhpSlim4ServerCodegen extends AbstractPhpCodegen { 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)) { diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache index 6d8d0bcbac28..f05b9c846c49 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/composer.mustache @@ -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", diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/config_dev_default.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/config_dev_default.mustache index 14c8fb4eb468..5d5845b3cc17 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/config_dev_default.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/config_dev_default.mustache @@ -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', diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/config_prod_default.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/config_prod_default.mustache index 57a1d3e128ed..ecbf7ab8987f 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/config_prod_default.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/config_prod_default.mustache @@ -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', diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/index.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/index.mustache index ead7d817f87c..18f2574b24ba 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/index.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/index.mustache @@ -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 @@ -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); diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/register_dependencies.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/register_dependencies.mustache index 0f48fb9de1cc..14e1d65f884f 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/register_dependencies.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/register_dependencies.mustache @@ -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( diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/register_routes.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/register_routes.mustache index b027ebc46e71..35bf34e3a0a0 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/register_routes.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/register_routes.mustache @@ -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; /** @@ -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); }; diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/response_emitter.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/response_emitter.mustache new file mode 100644 index 000000000000..ba6c985cc96f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/response_emitter.mustache @@ -0,0 +1,130 @@ +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); + } +} diff --git a/samples/server/petstore/php-slim4/.openapi-generator/FILES b/samples/server/petstore/php-slim4/.openapi-generator/FILES index 3bfc15a93cf2..6321cda0b3bc 100644 --- a/samples/server/petstore/php-slim4/.openapi-generator/FILES +++ b/samples/server/petstore/php-slim4/.openapi-generator/FILES @@ -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 diff --git a/samples/server/petstore/php-slim4/composer.json b/samples/server/petstore/php-slim4/composer.json index 4f31f4b28ff7..0e99d73f62c5 100644 --- a/samples/server/petstore/php-slim4/composer.json +++ b/samples/server/petstore/php-slim4/composer.json @@ -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", diff --git a/samples/server/petstore/php-slim4/config/dev/default.inc.php b/samples/server/petstore/php-slim4/config/dev/default.inc.php index 00ac63a03817..4bc496fc288d 100644 --- a/samples/server/petstore/php-slim4/config/dev/default.inc.php +++ b/samples/server/petstore/php-slim4/config/dev/default.inc.php @@ -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', diff --git a/samples/server/petstore/php-slim4/config/prod/default.inc.php b/samples/server/petstore/php-slim4/config/prod/default.inc.php index e2254223cf49..842df79d810a 100644 --- a/samples/server/petstore/php-slim4/config/prod/default.inc.php +++ b/samples/server/petstore/php-slim4/config/prod/default.inc.php @@ -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', diff --git a/samples/server/petstore/php-slim4/lib/App/RegisterDependencies.php b/samples/server/petstore/php-slim4/lib/App/RegisterDependencies.php index 8c21bc3dca68..0cd54563ed99 100644 --- a/samples/server/petstore/php-slim4/lib/App/RegisterDependencies.php +++ b/samples/server/petstore/php-slim4/lib/App/RegisterDependencies.php @@ -58,6 +58,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( diff --git a/samples/server/petstore/php-slim4/lib/App/RegisterRoutes.php b/samples/server/petstore/php-slim4/lib/App/RegisterRoutes.php index 52c5533f2ab7..4cb4f853ae11 100644 --- a/samples/server/petstore/php-slim4/lib/App/RegisterRoutes.php +++ b/samples/server/petstore/php-slim4/lib/App/RegisterRoutes.php @@ -24,6 +24,8 @@ declare(strict_types=1); */ namespace OpenAPIServer\App; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Slim\Exception\HttpNotImplementedException; /** @@ -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); }; diff --git a/samples/server/petstore/php-slim4/lib/App/ResponseEmitter.php b/samples/server/petstore/php-slim4/lib/App/ResponseEmitter.php new file mode 100644 index 000000000000..3923fef3153c --- /dev/null +++ b/samples/server/petstore/php-slim4/lib/App/ResponseEmitter.php @@ -0,0 +1,142 @@ +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); + } +} diff --git a/samples/server/petstore/php-slim4/public/index.php b/samples/server/petstore/php-slim4/public/index.php index e336d00f81b5..795b85efa0f2 100644 --- a/samples/server/petstore/php-slim4/public/index.php +++ b/samples/server/petstore/php-slim4/public/index.php @@ -27,8 +27,10 @@ use DI\ContainerBuilder; use OpenAPIServer\App\RegisterDependencies; use OpenAPIServer\App\RegisterRoutes; use OpenAPIServer\App\RegisterMiddlewares; +use OpenAPIServer\App\ResponseEmitter; +use Neomerx\Cors\Contracts\AnalyzerInterface; use Slim\Factory\ServerRequestCreatorFactory; -use Slim\ResponseEmitter; +use Slim\Middleware\ErrorMiddleware; // Instantiate PHP-DI ContainerBuilder $builder = new ContainerBuilder(); @@ -78,7 +80,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);