Completed
Pull Request — master (#43)
by
unknown
09:01 queued 07:19
created

createPreflightCorsResponseWithRouteOptions()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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