├── .module.ini ├── CHANGELOG.md ├── PWA.md ├── Plugin └── ControllerPlugin.php ├── README.md ├── Response └── Http.php ├── Test └── Integration │ ├── CheckResponseTest.php │ ├── ModuleTest.php │ └── PluginTest.php ├── Utils ├── RequestResponseValidator.php └── ResponseGenerator.php ├── composer.json ├── etc ├── acl.xml ├── adminhtml │ └── system.xml ├── config.xml ├── di.xml └── module.xml └── registration.php /.module.ini: -------------------------------------------------------------------------------- 1 | EXTENSION_VENDOR="Yireo" 2 | EXTENSION_NAME="CorsHack" 3 | COMPOSER_NAME="yireo-training/magento2-corshack" 4 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.0.6] - 29 July 2020 10 | ### Added 11 | - Magento 2.4 compatibility 12 | 13 | ## [1.0.5] - Undocumented 14 | -------------------------------------------------------------------------------- /PWA.md: -------------------------------------------------------------------------------- 1 | # PWA compatibility 2 | This extension adds CORS headers for benefit of PWA development. It does not need to be compatible with any PWA itself. 3 | -------------------------------------------------------------------------------- /Plugin/ControllerPlugin.php: -------------------------------------------------------------------------------- 1 | responseGenerator = $responseGenerator; 48 | $this->requestResponseValidator = $requestResponseValidator; 49 | } 50 | 51 | /** 52 | * @param Source $source 53 | * @param HttpResponse $response 54 | * @return HttpInterface 55 | */ 56 | public function afterDispatch(Source $source, ResponseInterface $response) 57 | { 58 | return $response; 59 | if ($this->requestResponseValidator->validate($response)) { 60 | $response->setStatusHeader(200); 61 | } 62 | 63 | return $this->responseGenerator->modifyResponse($response); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yireo CorsHack 2 | The new Magento 2 GraphQL system could be used with GraphQL clients 3 | (like Apollo or even Axios) to fetch data from Magento. Most of these 4 | clients use an HTTP request OPTIONS to see if CORS restrictions apply. 5 | This module adds an OPTIONS check to the GraphQL API. Also, this module 6 | adds Cross Origin headers (currently hard-coded to 7 | `http://localhost:3000`). 8 | 9 | ### Installation 10 | ``` 11 | composer require yireo-training/magento2-corshack 12 | ./bin/magento module:enable Yireo_CorsHack 13 | ``` 14 | 15 | ### Configuration 16 | Navigate to **Advanced > Yireo CorsHack** and add the schema + domain URL to the **Origin Domain** option. 17 | 18 | By default, a wildcard (`*`) is configured allowing all origin domains. 19 | 20 | Examples of values that can be configured: 21 | - * 22 | - https://yireo.com 23 | - http://localhost 24 | - http://localhost:3000 25 | 26 | In general the configuration value includes schema and domain name. It also includes the port number if it is not standard. 27 | -------------------------------------------------------------------------------- /Response/Http.php: -------------------------------------------------------------------------------- 1 | responseGenerator = $responseGenerator; 31 | $this->requestResponseValidator = $requestResponseValidator; 32 | } 33 | 34 | public function representJson($content) 35 | { 36 | if ($this->requestResponseValidator->validate($this)) { 37 | $this->setStatusHeader(200); 38 | } 39 | 40 | $this->responseGenerator->modifyResponse($this); 41 | 42 | return parent::representJson($content); 43 | } 44 | } -------------------------------------------------------------------------------- /Test/Integration/CheckResponseTest.php: -------------------------------------------------------------------------------- 1 | assertModuleIsEnabled('Magento_GraphQl'); 40 | $this->assertCorsHackOrigin('*'); 41 | $response = $this->sendGraphQlRequest(); 42 | 43 | /** @var Headers $headers */ 44 | $headers = $response->getHeaders(); 45 | $headersDump = var_export($this->getHeadersAsStringArray($headers), true); 46 | $this->assertSame(200, $response->getHttpResponseCode(), $headersDump); 47 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Origin'); 48 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Headers'); 49 | } 50 | 51 | 52 | /** 53 | * Test whether any response contains proper headers 54 | * 55 | * @magentoConfigFixture default/corshack/settings/origin * 56 | * @magentoAppArea graphql 57 | * @magentoDbIsolation disabled 58 | * @magentoCache full_page enabled 59 | */ 60 | public function testIfResponseContainsCrossOriginHeadersWithFpcEnabled() 61 | { 62 | $this->assertModuleIsEnabled('Magento_GraphQl'); 63 | $this->assertCorsHackOrigin('*'); 64 | $response = $this->sendGraphQlRequest(); 65 | 66 | $this->assertSame(200, $response->getHttpResponseCode()); 67 | $headers = $response->getHeaders(); 68 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Origin'); 69 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Headers'); 70 | 71 | // Redo the same request but now with FPC already warmed up 72 | // @todo: Double-check to see if caching headers are present 73 | $this->dispatch('/graphql'); 74 | $response = $this->getResponse(); 75 | $this->assertSame(200, $response->getHttpResponseCode()); 76 | $headers = $response->getHeaders(); 77 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Origin'); 78 | $this->assertHttpHeaderContains($headers, 'Access-Control-Allow-Headers'); 79 | } 80 | 81 | private function sendGraphQlRequest(): ResponseInterface 82 | { 83 | $request = $this->getRequest(); 84 | 85 | $headers = ObjectManager::getInstance()->create(Headers::class); 86 | $headers->addHeaderLine('Content-Type', 'application/json'); 87 | $request->setHeaders($headers); 88 | 89 | $query = $this->getQuery(); 90 | $data = ['query' => $query]; 91 | $content = json_encode($data); 92 | $request->setContent($content); 93 | $request->setMethod('POST'); 94 | $request->setPathInfo('/graphql'); 95 | 96 | $graphql = ObjectManager::getInstance()->get(GraphQl::class); 97 | return $graphql->dispatch($request); 98 | } 99 | 100 | /** 101 | * @param string $expected 102 | * @return void 103 | */ 104 | private function assertCorsHackOrigin(string $expected) 105 | { 106 | /** @var ScopeConfigInterface $scopeConfig */ 107 | $scopeConfig = ObjectManager::getInstance()->get(ScopeConfigInterface::class); 108 | $this->assertSame($expected, $scopeConfig->getValue('corshack/settings/origin')); 109 | } 110 | 111 | /** 112 | * @param Headers $headers 113 | * @param string $expectedHeader 114 | * @return void 115 | */ 116 | private function assertHttpHeaderContains(Headers $headers, string $expectedHeader) 117 | { 118 | $foundHeader = false; 119 | $headersAsString = []; 120 | foreach ($headers as $header) { 121 | $headersAsString[] = $header->getFieldName(); 122 | if ($header->getFieldName() === $expectedHeader) { 123 | $foundHeader = true; 124 | } 125 | } 126 | 127 | $this->assertTrue($headersAsString > 0); 128 | $msg = $expectedHeader . ' not found: ' . var_export($headersAsString, true); 129 | $this->assertTrue($foundHeader, $msg); 130 | } 131 | 132 | /** 133 | * @param Headers $headers 134 | * @return array 135 | */ 136 | private function getHeadersAsStringArray(Headers $headers): array 137 | { 138 | $headersAsString = []; 139 | foreach ($headers as $header) { 140 | $headersAsString[] = $header->getFieldName() . ': ' . $header->getFieldValue(); 141 | } 142 | 143 | return $headersAsString; 144 | } 145 | 146 | /** 147 | * @return string 148 | */ 149 | private function getQuery(): string 150 | { 151 | return <<assertModuleIsEnabled('Yireo_CorsHack'); 28 | $this->assertModuleIsRegistered('Yireo_CorsHack'); 29 | $this->assertModuleIsRegisteredForReal('Yireo_CorsHack'); 30 | } 31 | } -------------------------------------------------------------------------------- /Test/Integration/PluginTest.php: -------------------------------------------------------------------------------- 1 | assertInterceptorPluginIsRegistered( 26 | GraphQl::class, 27 | ControllerPlugin::class, 28 | 'Yireo_CorsHack::addHeadersToResponse' 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Utils/RequestResponseValidator.php: -------------------------------------------------------------------------------- 1 | request = $request; 37 | $this->serializer = $serializer; 38 | } 39 | 40 | /** 41 | * @return bool 42 | */ 43 | public function validate(ResponseInterface $response): bool 44 | { 45 | if ($this->request->getMethod() === 'OPTIONS') { 46 | return true; 47 | } 48 | 49 | if ($this->request->getMethod() === 'GET') { 50 | return true; 51 | } 52 | 53 | $data = $this->serializer->unserialize($response->getBody()); 54 | if (!empty($data) && !empty($data['data']) && empty($data['errors'])) { 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Utils/ResponseGenerator.php: -------------------------------------------------------------------------------- 1 | scopeConfig = $scopeConfig; 29 | } 30 | 31 | /** 32 | * @param HttpResponse $response 33 | * @return HttpResponse 34 | */ 35 | public function modifyResponse(HttpResponse $response): HttpResponse 36 | { 37 | $domains = $this->getAccessControlAllowOriginDomains(); 38 | $headers = $this->getAccessControlAllowHeaders(); 39 | 40 | //$response->setHeader('X-Yireo-CorsHack', 1); // @todo: Add a setting for this 41 | $response->setHeader('Access-Control-Allow-Origin', implode(', ', $domains)); 42 | $response->setHeader('Access-Control-Allow-Headers', implode(',', $headers)); 43 | $response->setHeader('Access-Control-Allow-Credentials', 'true'); 44 | $response->setHeader('Access-Control-Allow-Method', 'POST, GET, OPTIONS'); 45 | $response->setHeader('Access-Control-Max-Age', '86400'); 46 | 47 | return $response; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | private function getAccessControlAllowOriginDomains(): array 54 | { 55 | $domains = []; 56 | 57 | $storedOrigins = (string)$this->scopeConfig->getValue('corshack/settings/origin'); 58 | $storedOrigins = explode(',', $storedOrigins); 59 | foreach ($storedOrigins as $storedOrigin) { 60 | $storedOrigin = trim($storedOrigin); 61 | if (!empty($storedOrigin)) { 62 | $domains[] = $storedOrigin; 63 | } 64 | } 65 | 66 | $domains = array_unique($domains); 67 | 68 | if (count($domains) > 1) { 69 | $domains = ['*']; 70 | } 71 | 72 | return $domains; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | private function getAccessControlAllowHeaders(): array 79 | { 80 | $headers = []; 81 | //$headers[] = 'Overwrite'; 82 | //$headers[] = 'Destination'; 83 | //$headers[] = 'Depth'; 84 | $headers[] = 'Content-Type'; 85 | //$headers[] = 'User-Agent'; 86 | //$headers[] = 'X-File-Size'; 87 | //$headers[] = 'X-Requested-With'; 88 | //$headers[] = 'If-Modified-Since'; 89 | //$headers[] = 'X-File-Name'; 90 | //$headers[] = 'Cache-Control'; 91 | $headers[] = 'Authorization'; 92 | 93 | return $headers; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yireo/magento2-corshack", 3 | "license": "OSL-3.0", 4 | "type": "magento2-module", 5 | "version": "1.0.6", 6 | "homepage": "https://github.com/yireo-training/magento2-corshack", 7 | "description": "Magento 2 module to add some hacks to GraphQL API", 8 | "keywords":[ "composer-installer", "magento"], 9 | "authors": [ 10 | { 11 | "name": "Jisse Reitsma (Yireo)", 12 | "email": "jisse@yireo.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.2.0", 17 | "ext-json": "*" 18 | }, 19 | "require-dev":{ 20 | "phpunit/phpunit":"*", 21 | "yireo/magento2-integration-test-helper": "*", 22 | "composer/composer":"*@dev" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Yireo\\CorsHack\\": "" 27 | }, 28 | "files": [ 29 | "registration.php" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /etc/acl.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 |
15 | 16 | advanced 17 | Yireo_CorsHack::config 18 | 19 | 20 | 21 | 22 | 23 | Add a URL like http://localhost:3000, or a wildcard *. Note that a comma-separated list does not work in the browser. If you have multiple domains, use a wildcard instead. 24 | 25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | * 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | Yireo\CorsHack\Response\Http 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 |