From 01069804756574c6c5a5a80737035074f7655a0c Mon Sep 17 00:00:00 2001 From: George Steel Date: Thu, 24 Mar 2022 12:56:43 +0000 Subject: [PATCH] Introduce factory for the server url helper Adds laminas-diactoros as a dependency so that detection of the host URI can be delegated to existing code Signed-off-by: George Steel --- composer.json | 1 + composer.lock | 209 ++++++++++++++++++- src/Helper/Service/ServerUrlFactory.php | 56 +++++ src/HelperPluginManager.php | 2 +- test/Helper/Service/ServerUrlFactoryTest.php | 89 ++++++++ 5 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 src/Helper/Service/ServerUrlFactory.php create mode 100644 test/Helper/Service/ServerUrlFactoryTest.php diff --git a/composer.json b/composer.json index 2fcc7df3e..770aa074c 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "ext-filter": "*", "ext-json": "*", "container-interop/container-interop": "^1.2", + "laminas/laminas-diactoros": "^2.8", "laminas/laminas-escaper": "^2.5", "laminas/laminas-eventmanager": "^3.4", "laminas/laminas-json": "^3.3", diff --git a/composer.lock b/composer.lock index f02a21769..8cf8d2a41 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "adb0342243aa19eb2796daf3fd170b5a", + "content-hash": "ad0a45609abcabfa5477733811c7dac7", "packages": [ { "name": "container-interop/container-interop", @@ -42,6 +42,105 @@ "abandoned": "psr/container", "time": "2017-02-14T19:40:03+00:00" }, + { + "name": "laminas/laminas-diactoros", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "0c26ef1d95b6d7e6e3943a243ba3dc0797227199" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/0c26ef1d95b6d7e6e3943a243ba3dc0797227199", + "reference": "0c26ef1d95b6d7e6e3943a243ba3dc0797227199", + "shasum": "" + }, + "require": { + "php": "^7.3 || ~8.0.0 || ~8.1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0" + }, + "conflict": { + "phpspec/prophecy": "<1.9.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^0.8.0", + "laminas/laminas-coding-standard": "~1.0.0", + "php-http/psr7-integration-tests": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.1", + "psalm/plugin-phpunit": "^0.14.0", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "laminas": { + "config-provider": "Laminas\\Diactoros\\ConfigProvider", + "module": "Laminas\\Diactoros" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php", + "src/functions/create_uploaded_file.legacy.php", + "src/functions/marshal_headers_from_sapi.legacy.php", + "src/functions/marshal_method_from_sapi.legacy.php", + "src/functions/marshal_protocol_version_from_sapi.legacy.php", + "src/functions/marshal_uri_from_sapi.legacy.php", + "src/functions/normalize_server.legacy.php", + "src/functions/normalize_uploaded_files.legacy.php", + "src/functions/parse_cookie_header.legacy.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-22T03:54:36+00:00" + }, { "name": "laminas/laminas-escaper", "version": "2.9.0", @@ -423,6 +522,114 @@ "source": "https://github.com/php-fig/container/tree/1.1.2" }, "time": "2021-11-05T16:50:12+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" } ], "packages-dev": [ diff --git a/src/Helper/Service/ServerUrlFactory.php b/src/Helper/Service/ServerUrlFactory.php new file mode 100644 index 000000000..b6a2cc803 --- /dev/null +++ b/src/Helper/Service/ServerUrlFactory.php @@ -0,0 +1,56 @@ +fetchConfiguredServerUrl($container) ?: $this->detectServerUrlFromEnvironment() + ); + } + + private function fetchConfiguredServerUrl(ContainerInterface $container): ?string + { + $config = $container->has('config') ? $container->get('config') : []; + assert(is_array($config) || $config instanceof ArrayAccess); + + $helperConfig = $config['view_helper_config'] ?? []; + assert(is_array($helperConfig)); + + $serverUrl = $helperConfig['server_url'] ?? null; + assert(is_string($serverUrl) || $serverUrl === null); + + return $serverUrl; + } + + private function detectServerUrlFromEnvironment(): string + { + $uri = marshalUriFromSapi($_SERVER, marshalHeadersFromSapi($_SERVER)) + ->withPath('') + ->withQuery('') + ->withFragment(''); + + if (! $uri->getHost() || ! $uri->getScheme()) { + throw new RuntimeException( + 'The current host or scheme cannot be detected from the environment' + ); + } + + return (string) $uri; + } +} diff --git a/src/HelperPluginManager.php b/src/HelperPluginManager.php index ca1c656f2..bf158d215 100644 --- a/src/HelperPluginManager.php +++ b/src/HelperPluginManager.php @@ -281,7 +281,7 @@ class HelperPluginManager extends AbstractPluginManager Helper\Placeholder::class => InvokableFactory::class, Helper\RenderChildModel::class => InvokableFactory::class, Helper\RenderToPlaceholder::class => InvokableFactory::class, - Helper\ServerUrl::class => InvokableFactory::class, + Helper\ServerUrl::class => Helper\Service\ServerUrlFactory::class, Helper\Url::class => InvokableFactory::class, Helper\ViewModel::class => InvokableFactory::class, diff --git a/test/Helper/Service/ServerUrlFactoryTest.php b/test/Helper/Service/ServerUrlFactoryTest.php new file mode 100644 index 000000000..606406b01 --- /dev/null +++ b/test/Helper/Service/ServerUrlFactoryTest.php @@ -0,0 +1,89 @@ + */ + private array $serverVariables; + + protected function setUp(): void + { + parent::setUp(); + $this->serverVariables = $_SERVER; + } + + protected function tearDown(): void + { + $_SERVER = $this->serverVariables; + parent::tearDown(); + } + + public function testThatWhenThereIsNoConfigurationTheHostUriWillBeDetectedFromGlobals(): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = true; + $_SERVER['SERVER_PORT'] = 443; + + $helper = (new ServerUrlFactory())(new ServiceManager()); + self::assertEquals('https://example.com', $helper->__invoke()); + } + + public function testThatWhenThereIsNoConfigurationDetectedPathQueryAndFragmentWillBeOmitted(): void + { + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = true; + $_SERVER['SERVER_PORT'] = 443; + $_SERVER['REQUEST_URI'] = '/some/#thing?foo=bar'; + + $helper = (new ServerUrlFactory())(new ServiceManager()); + self::assertEquals('https://example.com', $helper->__invoke()); + } + + public function testThatConfiguredHostIsPreferred(): void + { + $container = new ServiceManager(); + $container->setService('config', [ + 'view_helper_config' => [ + 'server_url' => 'https://other.example.com', + ], + ]); + + $_SERVER['HTTP_HOST'] = 'example.com'; + $_SERVER['HTTPS'] = true; + $_SERVER['SERVER_PORT'] = 443; + + $helper = (new ServerUrlFactory())($container); + self::assertEquals('https://other.example.com', $helper->__invoke()); + } + + public function testUndetectableEnvironmentAndZeroConfigurationYieldsException(): void + { + $_SERVER = []; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The current host or scheme cannot be detected from the environment'); + + (new ServerUrlFactory())(new ServiceManager()); + } + + public function testAnEmptyEnvironmentIsAcceptableWhenConfigurationIsFound(): void + { + $_SERVER = []; + $container = new ServiceManager(); + $container->setService('config', [ + 'view_helper_config' => [ + 'server_url' => 'https://other.example.com', + ], + ]); + $helper = (new ServerUrlFactory())($container); + self::assertEquals('https://other.example.com', $helper->__invoke()); + } +}