CorsService   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 213
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 30
lcom 1
cbo 10
dl 0
loc 213
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A isCorsRequest() 0 24 5
A isPreflightRequest() 0 6 3
A createPreflightCorsResponse() 0 19 2
A createPreflightCorsResponseWithRouteOptions() 0 10 4
A getAllowedOriginValue() 0 20 5
A ensureVaryHeader() 0 22 3
B populateCorsResponse() 0 36 7
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license.
17
 */
18
19
namespace ZfrCors\Service;
20
21
use Zend\Mvc\Router\Http\RouteMatch as DeprecatedRouteMatch;
22
use Zend\Router\Http\RouteMatch;
23
use Zend\Http\Header;
24
use Zend\Uri\UriFactory;
25
use ZfrCors\Exception\DisallowedOriginException;
26
use ZfrCors\Exception\InvalidOriginException;
27
use ZfrCors\Options\CorsOptions;
28
use Zend\Http\Request as HttpRequest;
29
use Zend\Http\Response as HttpResponse;
30
31
/**
32
 * Service that offers a simple mechanism to handle CORS requests
33
 *
34
 * This service closely follow the specification here: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
35
 *
36
 * @license MIT
37
 * @author  Florent Blaison <[email protected]>
38
 */
39
class CorsService
40
{
41
    /**
42
     * @var CorsOptions
43
     */
44
    protected $options;
45
46
    /**
47
     * @param CorsOptions $options
48
     */
49
    public function __construct(CorsOptions $options)
50
    {
51
        $this->options = $options;
52
    }
53
54
    /**
55
     * Check if the HTTP request is a CORS request by checking if the Origin header is present and that the
56
     * request URI is not the same as the one in the Origin
57
     *
58
     * @param  HttpRequest $request
59
     * @return bool
60
     */
61
    public function isCorsRequest(HttpRequest $request)
62
    {
63
        $headers = $request->getHeaders();
64
65
        if (! $headers->has('Origin')) {
66
            return false;
67
        }
68
69
        $origin = $headers->get('Origin');
70
71
        if (! $origin instanceof Header\Origin) {
72
            throw InvalidOriginException::fromInvalidHeaderValue();
73
        }
74
75
        $originUri  = UriFactory::factory($origin->getFieldValue());
76
        $requestUri = $request->getUri();
77
78
        // According to the spec (http://tools.ietf.org/html/rfc6454#section-4), we should check host, port and scheme
79
80
        return (! ($originUri->getHost() === $requestUri->getHost())
81
            || ! ($originUri->getPort() === $requestUri->getPort())
82
            || ! ($originUri->getScheme() === $requestUri->getScheme())
83
        );
84
    }
85
86
    /**
87
     * Check if the CORS request is a preflight request
88
     *
89
     * @param  HttpRequest $request
90
     * @return bool
91
     */
92
    public function isPreflightRequest(HttpRequest $request)
93
    {
94
        return $this->isCorsRequest($request)
95
            && strtoupper($request->getMethod()) === 'OPTIONS'
96
            && $request->getHeaders()->has('Access-Control-Request-Method');
97
    }
98
99
    /**
100
     * Create a preflight response by adding the corresponding headers
101
     *
102
     * @param  HttpRequest  $request
103
     * @return HttpResponse
104
     */
105
    public function createPreflightCorsResponse(HttpRequest $request)
106
    {
107
        $response = new HttpResponse();
108
        $response->setStatusCode(200);
109
110
        $headers = $response->getHeaders();
111
112
        $headers->addHeaderLine('Access-Control-Allow-Origin', $this->getAllowedOriginValue($request));
113
        $headers->addHeaderLine('Access-Control-Allow-Methods', implode(', ', $this->options->getAllowedMethods()));
114
        $headers->addHeaderLine('Access-Control-Allow-Headers', implode(', ', $this->options->getAllowedHeaders()));
115
        $headers->addHeaderLine('Access-Control-Max-Age', $this->options->getMaxAge());
116
        $headers->addHeaderLine('Content-Length', 0);
117
118
        if ($this->options->getAllowedCredentials()) {
119
            $headers->addHeaderLine('Access-Control-Allow-Credentials', 'true');
120
        }
121
122
        return $response;
123
    }
124
125
    /**
126
     * Create a preflight response by adding the correspoding headers which are merged with per-route configuration
127
     *
128
     * @param HttpRequest                          $request
129
     * @param RouteMatch|DeprecatedRouteMatch|null $routeMatch
130
     *
131
     * @return HttpResponse
132
     */
133
    public function createPreflightCorsResponseWithRouteOptions(HttpRequest $request, $routeMatch = null)
134
    {
135
        $options = $this->options;
136
        if ($routeMatch instanceof RouteMatch || $routeMatch instanceof DeprecatedRouteMatch) {
0 ignored issues
show
Bug introduced by
The class Zend\Router\Http\RouteMatch does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
137
            $options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []);
138
        }
139
        $response = $this->createPreflightCorsResponse($request);
140
141
        return $response;
142
    }
143
144
    /**
145
     * Populate a simple CORS response
146
     *
147
     * @param  HttpRequest               $request
148
     * @param  HttpResponse              $response
149
     * @param  null|RouteMatch           $routeMatch
150
     * @return HttpResponse
151
     * @throws DisallowedOriginException If the origin is not allowed
152
     */
153
    public function populateCorsResponse(HttpRequest $request, HttpResponse $response, $routeMatch = null)
154
    {
155
        if ($routeMatch instanceof RouteMatch || $routeMatch instanceof DeprecatedRouteMatch) {
0 ignored issues
show
Bug introduced by
The class Zend\Router\Http\RouteMatch does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
156
            $this->options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []);
157
        }
158
159
        $origin = $this->getAllowedOriginValue($request);
160
161
        // If $origin is "null", then it means that the origin is not allowed. As this is
162
        // a simple request, it is useless to continue the processing as it will be refused
163
        // by the browser anyway, so we throw an exception
164
        if ($origin === 'null') {
165
            $origin = $request->getHeader('Origin');
166
            $originHeader = $origin ? $origin->getFieldValue() : '';
0 ignored issues
show
Bug introduced by
The method getFieldValue does only exist in Zend\Http\Header\HeaderInterface, but not in ArrayIterator.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
167
            throw new DisallowedOriginException(
168
                sprintf(
169
                    'The origin "%s" is not authorized',
170
                    $originHeader
171
                )
172
            );
173
        }
174
175
        $headers = $response->getHeaders();
176
        $headers->addHeaderLine('Access-Control-Allow-Origin', $origin);
177
        $headers->addHeaderLine('Access-Control-Expose-Headers', implode(', ', $this->options->getExposedHeaders()));
178
179
        $headers = $this->ensureVaryHeader($response);
180
181
        if ($this->options->getAllowedCredentials()) {
182
            $headers->addHeaderLine('Access-Control-Allow-Credentials', 'true');
183
        }
184
185
        $response->setHeaders($headers);
186
187
        return $response;
188
    }
189
190
    /**
191
     * Get a single value for the "Access-Control-Allow-Origin" header
192
     *
193
     * According to the spec, it is not valid to set multiple origins separated by commas. Only accepted
194
     * value are wildcard ("*"), an exact domain or a null string.
195
     *
196
     * @link http://www.w3.org/TR/cors/#access-control-allow-origin-response-header
197
     * @param  HttpRequest $request
198
     * @return string
199
     */
200
    protected function getAllowedOriginValue(HttpRequest $request)
201
    {
202
        $allowedOrigins = $this->options->getAllowedOrigins();
203
204
        $origin = $request->getHeader('Origin');
205
206
        if ($origin) {
207
            $origin = $origin->getFieldValue();
0 ignored issues
show
Bug introduced by
The method getFieldValue does only exist in Zend\Http\Header\HeaderInterface, but not in ArrayIterator.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
208
            if (in_array('*', $allowedOrigins)) {
209
                return $origin;
210
            }
211
            foreach ($allowedOrigins as $allowedOrigin) {
212
                if (fnmatch($allowedOrigin, $origin)) {
213
                    return $origin;
214
                }
215
            }
216
        }
217
218
        return 'null';
219
    }
220
221
    /**
222
     * Ensure that the Vary header is set.
223
     *
224
     *
225
     * @link http://www.w3.org/TR/cors/#resource-implementation
226
     * @param HttpResponse $response
227
     * @return \Zend\Http\Headers
228
     */
229
    public function ensureVaryHeader(HttpResponse $response)
230
    {
231
        $headers = $response->getHeaders();
232
        // If the origin is not "*", we should add the "Origin" value to the "Vary" header
233
        // See more: http://www.w3.org/TR/cors/#resource-implementation
234
        $allowedOrigins = $this->options->getAllowedOrigins();
235
236
        if (in_array('*', $allowedOrigins)) {
237
            return $headers;
238
        }
239
        if ($headers->has('Vary')) {
240
            $varyHeader = $headers->get('Vary');
241
            $varyValue  = $varyHeader->getFieldValue() . ', Origin';
0 ignored issues
show
Bug introduced by
The method getFieldValue does only exist in Zend\Http\Header\HeaderInterface, but not in ArrayIterator.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
242
243
            $headers->removeHeader($varyHeader);
0 ignored issues
show
Bug introduced by
It seems like $varyHeader defined by $headers->get('Vary') on line 240 can also be of type boolean or object<ArrayIterator>; however, Zend\Http\Headers::removeHeader() does only seem to accept object<Zend\Http\Header\HeaderInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
244
            $headers->addHeaderLine('Vary', $varyValue);
245
        } else {
246
            $headers->addHeaderLine('Vary', 'Origin');
247
        }
248
249
        return $headers;
250
    }
251
}
252