Completed
Pull Request — master (#43)
by
unknown
01:40
created

createPreflightCorsResponseWithRouteOptions()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 10
rs 9.2
cc 4
eloc 6
nc 2
nop 2
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\Uri\UriFactory;
24
use ZfrCors\Exception\DisallowedOriginException;
25
use ZfrCors\Options\CorsOptions;
26
use Zend\Http\Request as HttpRequest;
27
use Zend\Http\Response as HttpResponse;
28
29
/**
30
 * Service that offers a simple mechanism to handle CORS requests
31
 *
32
 * This service closely follow the specification here: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
33
 *
34
 * @license MIT
35
 * @author  Florent Blaison <[email protected]>
36
 */
37
class CorsService
38
{
39
    /**
40
     * @var CorsOptions
41
     */
42
    protected $options;
43
44
    /**
45
     * @param CorsOptions $options
46
     */
47
    public function __construct(CorsOptions $options)
48
    {
49
        $this->options = $options;
50
    }
51
52
    /**
53
     * Check if the HTTP request is a CORS request by checking if the Origin header is present and that the
54
     * request URI is not the same as the one in the Origin
55
     *
56
     * @param  HttpRequest $request
57
     * @return bool
58
     */
59
    public function isCorsRequest(HttpRequest $request)
60
    {
61
        $headers = $request->getHeaders();
62
63
        if (! $headers->has('Origin')) {
64
            return false;
65
        }
66
67
        $originUri  = UriFactory::factory($headers->get('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...
68
        $requestUri = $request->getUri();
69
70
        // According to the spec (http://tools.ietf.org/html/rfc6454#section-4), we should check host, port and scheme
71
72
        return (! ($originUri->getHost() === $requestUri->getHost())
73
            || ! ($originUri->getPort() === $requestUri->getPort())
74
            || ! ($originUri->getScheme() === $requestUri->getScheme())
75
        );
76
    }
77
78
    /**
79
     * Check if the CORS request is a preflight request
80
     *
81
     * @param  HttpRequest $request
82
     * @return bool
83
     */
84
    public function isPreflightRequest(HttpRequest $request)
85
    {
86
        return $this->isCorsRequest($request)
87
            && strtoupper($request->getMethod()) === 'OPTIONS'
88
            && $request->getHeaders()->has('Access-Control-Request-Method');
89
    }
90
91
    /**
92
     * Create a preflight response by adding the corresponding headers
93
     *
94
     * @param  HttpRequest  $request
95
     * @return HttpResponse
96
     */
97
    public function createPreflightCorsResponse(HttpRequest $request)
98
    {
99
        $response = new HttpResponse();
100
        $response->setStatusCode(200);
101
102
        $headers = $response->getHeaders();
103
104
        $headers->addHeaderLine('Access-Control-Allow-Origin', $this->getAllowedOriginValue($request));
105
        $headers->addHeaderLine('Access-Control-Allow-Methods', implode(', ', $this->options->getAllowedMethods()));
106
        $headers->addHeaderLine('Access-Control-Allow-Headers', implode(', ', $this->options->getAllowedHeaders()));
107
        $headers->addHeaderLine('Access-Control-Max-Age', $this->options->getMaxAge());
108
        $headers->addHeaderLine('Content-Length', 0);
109
110
        if ($this->options->getAllowedCredentials()) {
111
            $headers->addHeaderLine('Access-Control-Allow-Credentials', 'true');
112
        }
113
114
        return $response;
115
    }
116
117
    /**
118
     * Create a preflight response by adding the correspoding headers which are merged with per-route configuration
119
     *
120
     * @param HttpRequest                          $request
121
     * @param RouteMatch|DeprecatedRouteMatch|null $routeMatch
122
     *
123
     * @return HttpResponse
124
     */
125
    public function createPreflightCorsResponseWithRouteOptions(HttpRequest $request, $routeMatch = null)
126
    {
127
        $options = $this->options;
128
        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...
129
            $options->setFromArray($routeMatch->getParam(CorsOptions::ROUTE_PARAM) ?: []);
130
        }
131
        $response = $this->createPreflightCorsResponse($request);
132
133
        return $response;
134
    }
135
136
    /**
137
     * Populate a simple CORS response
138
     *
139
     * @param  HttpRequest               $request
140
     * @param  HttpResponse              $response
141
     * @return HttpResponse
142
     * @throws DisallowedOriginException If the origin is not allowed
143
     */
144
    public function populateCorsResponse(HttpRequest $request, HttpResponse $response)
145
    {
146
        $origin = $this->getAllowedOriginValue($request);
147
148
        // If $origin is "null", then it means than the origin is not allowed. As this is
149
        // a simple request, it is useless to continue the processing as it will be refused
150
        // by the browser anyway, so we throw an exception
151
        if ($origin === 'null') {
152
            $origin = $request->getHeader('Origin');
153
            $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...
154
            throw new DisallowedOriginException(
155
                sprintf(
156
                    'The origin "%s" is not authorized',
157
                    $originHeader
158
                )
159
            );
160
        }
161
162
        $headers = $response->getHeaders();
163
        $headers->addHeaderLine('Access-Control-Allow-Origin', $origin);
164
        $headers->addHeaderLine('Access-Control-Expose-Headers', implode(', ', $this->options->getExposedHeaders()));
165
166
        $headers = $this->ensureVaryHeader($response);
167
168
        if ($this->options->getAllowedCredentials()) {
169
            $headers->addHeaderLine('Access-Control-Allow-Credentials', 'true');
170
        }
171
172
        $response->setHeaders($headers);
173
174
        return $response;
175
    }
176
177
    /**
178
     * Get a single value for the "Access-Control-Allow-Origin" header
179
     *
180
     * According to the spec, it is not valid to set multiple origins separated by commas. Only accepted
181
     * value are wildcard ("*"), an exact domain or a null string.
182
     *
183
     * @link http://www.w3.org/TR/cors/#access-control-allow-origin-response-header
184
     * @param  HttpRequest $request
185
     * @return string
186
     */
187
    protected function getAllowedOriginValue(HttpRequest $request)
188
    {
189
        $allowedOrigins = $this->options->getAllowedOrigins();
190
191
        if (in_array('*', $allowedOrigins)) {
192
            return '*';
193
        }
194
195
        $origin = $request->getHeader('Origin');
196
197
        if ($origin) {
198
            $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...
199
            foreach ($allowedOrigins as $allowedOrigin) {
200
                if (fnmatch($allowedOrigin, $origin)) {
201
                    return $origin;
202
                }
203
            }
204
        }
205
206
        return 'null';
207
    }
208
209
    /**
210
     * Ensure that the Vary header is set.
211
     *
212
     *
213
     * @link http://www.w3.org/TR/cors/#resource-implementation
214
     * @param HttpResponse $response
215
     * @return \Zend\Http\Headers
216
     */
217
    public function ensureVaryHeader(HttpResponse $response)
218
    {
219
        $headers = $response->getHeaders();
220
        // If the origin is not "*", we should add the "Origin" value to the "Vary" header
221
        // See more: http://www.w3.org/TR/cors/#resource-implementation
222
        $allowedOrigins = $this->options->getAllowedOrigins();
223
224
        if (in_array('*', $allowedOrigins)) {
225
            return $headers;
226
        }
227
        if ($headers->has('Vary')) {
228
            $varyHeader = $headers->get('Vary');
229
            $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...
230
231
            $headers->removeHeader($varyHeader);
0 ignored issues
show
Bug introduced by
It seems like $varyHeader defined by $headers->get('Vary') on line 228 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...
232
            $headers->addHeaderLine('Vary', $varyValue);
233
        } else {
234
            $headers->addHeaderLine('Vary', 'Origin');
235
        }
236
237
        return $headers;
238
    }
239
}
240