WebResponse   D
last analyzed

Complexity

Total Complexity 106

Size/Duplication

Total Lines 759
Duplicated Lines 4.74 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
dl 36
loc 759
rs 4.4444
c 0
b 0
f 0
wmc 106
lcom 1
cbo 7

29 Methods

Rating   Name   Duplication   Size   Complexity  
D initialize() 0 35 10
C send() 0 24 10
A sendContent() 0 11 4
A clear() 0 8 1
A hasContent() 0 4 2
A setContentType() 0 4 1
A getContentType() 0 9 3
B merge() 0 21 8
A validateHttpStatusCode() 0 5 1
A setHttpStatusCode() 5 9 2
A getHttpStatusCode() 0 4 1
A normalizeHttpHeaderName() 0 10 3
A getHttpHeader() 9 9 2
A getHttpHeaders() 0 4 1
A hasHttpHeader() 9 9 2
A setHttpHeader() 0 12 4
C setCookie() 0 23 9
A unsetCookie() 0 6 1
A getCookie() 0 6 2
A hasCookie() 0 4 1
A removeCookie() 0 6 2
A getCookies() 0 4 1
A removeHttpHeader() 10 10 2
A clearHttpHeaders() 0 4 1
F sendHttpResponseHeaders() 0 82 27
A setRedirect() 3 7 2
A getRedirect() 0 4 1
A hasRedirect() 0 4 1
A clearRedirect() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like WebResponse often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WebResponse, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Agavi\Response;
3
4
// +---------------------------------------------------------------------------+
5
// | This file is part of the Agavi package.                                   |
6
// | Copyright (c) 2005-2011 the Agavi Project.                                |
7
// |                                                                           |
8
// | For the full copyright and license information, please view the LICENSE   |
9
// | file that was distributed with this source code. You can also view the    |
10
// | LICENSE file online at http://www.agavi.org/LICENSE.txt                   |
11
// |   vi: set noexpandtab:                                                    |
12
// |   Local Variables:                                                        |
13
// |   indent-tabs-mode: t                                                     |
14
// |   End:                                                                    |
15
// +---------------------------------------------------------------------------+
16
use Agavi\Config\Config;
17
use Agavi\Core\Context;
18
use Agavi\Dispatcher\OutputType;
19
use Agavi\Exception\AgaviException;
20
use Agavi\Request\WebRequest;
21
use Agavi\Routing\WebRouting;
22
23
/**
24
 * AgaviWebResponse handles HTTP responses.
25
 *
26
 * @package    agavi
27
 * @subpackage response
28
 *
29
 * @author     David Zülke <[email protected]>
30
 * @copyright  Authors
31
 * @copyright  The Agavi Project
32
 *
33
 * @since      0.11.0
34
 *
35
 * @version    $Id$
36
 */
37
class WebResponse extends Response
38
{
39
    /**
40
     * @var        array An array of all HTTP 1.0 status codes and their message.
41
     */
42
    protected $http10StatusCodes = array(
43
        '200' => "HTTP/1.0 200 OK",
44
        '201' => "HTTP/1.0 201 Created",
45
        '202' => "HTTP/1.0 202 Accepted",
46
        '204' => "HTTP/1.0 204 No Content",
47
        '205' => "HTTP/1.0 205 Reset Content",
48
        '206' => "HTTP/1.0 206 Partial Content",
49
        '300' => "HTTP/1.0 300 Multiple Choices",
50
        '301' => "HTTP/1.0 301 Moved Permanently",
51
        '302' => "HTTP/1.0 302 Found",
52
        '304' => "HTTP/1.0 304 Not Modified",
53
        '400' => "HTTP/1.0 400 Bad Request",
54
        '401' => "HTTP/1.0 401 Unauthorized",
55
        '402' => "HTTP/1.0 402 Payment Required",
56
        '403' => "HTTP/1.0 403 Forbidden",
57
        '404' => "HTTP/1.0 404 Not Found",
58
        '405' => "HTTP/1.0 405 Method Not Allowed",
59
        '406' => "HTTP/1.0 406 Not Acceptable",
60
        '407' => "HTTP/1.0 407 Proxy Authentication Required",
61
        '408' => "HTTP/1.0 408 Request Timeout",
62
        '409' => "HTTP/1.0 409 Conflict",
63
        '410' => "HTTP/1.0 410 Gone",
64
        '411' => "HTTP/1.0 411 Length Required",
65
        '412' => "HTTP/1.0 412 Precondition Failed",
66
        '413' => "HTTP/1.0 413 Request Entity Too Large",
67
        '414' => "HTTP/1.0 414 Request-URI Too Long",
68
        '415' => "HTTP/1.0 415 Unsupported Media Type",
69
        '416' => "HTTP/1.0 416 Requested Range Not Satisfiable",
70
        '417' => "HTTP/1.0 417 Expectation Failed",
71
        '500' => "HTTP/1.0 500 Internal Server Error",
72
        '501' => "HTTP/1.0 501 Not Implemented",
73
        '502' => "HTTP/1.0 502 Bad Gateway",
74
        '503' => "HTTP/1.0 503 Service Unavailable",
75
        '504' => "HTTP/1.0 504 Gateway Timeout",
76
        '505' => "HTTP/1.0 505 HTTP Version Not Supported",
77
    );
78
    
79
    /**
80
     * @var        array An array of all HTTP 1.1 status codes and their message.
81
     */
82
    protected $http11StatusCodes = array(
83
        '100' => "HTTP/1.1 100 Continue",
84
        '101' => "HTTP/1.1 101 Switching Protocols",
85
        '200' => "HTTP/1.1 200 OK",
86
        '201' => "HTTP/1.1 201 Created",
87
        '202' => "HTTP/1.1 202 Accepted",
88
        '203' => "HTTP/1.1 203 Non-Authoritative Information",
89
        '204' => "HTTP/1.1 204 No Content",
90
        '205' => "HTTP/1.1 205 Reset Content",
91
        '206' => "HTTP/1.1 206 Partial Content",
92
        '300' => "HTTP/1.1 300 Multiple Choices",
93
        '301' => "HTTP/1.1 301 Moved Permanently",
94
        '302' => "HTTP/1.1 302 Found",
95
        '303' => "HTTP/1.1 303 See Other",
96
        '304' => "HTTP/1.1 304 Not Modified",
97
        '305' => "HTTP/1.1 305 Use Proxy",
98
        '307' => "HTTP/1.1 307 Temporary Redirect",
99
        '400' => "HTTP/1.1 400 Bad Request",
100
        '401' => "HTTP/1.1 401 Unauthorized",
101
        '402' => "HTTP/1.1 402 Payment Required",
102
        '403' => "HTTP/1.1 403 Forbidden",
103
        '404' => "HTTP/1.1 404 Not Found",
104
        '405' => "HTTP/1.1 405 Method Not Allowed",
105
        '406' => "HTTP/1.1 406 Not Acceptable",
106
        '407' => "HTTP/1.1 407 Proxy Authentication Required",
107
        '408' => "HTTP/1.1 408 Request Timeout",
108
        '409' => "HTTP/1.1 409 Conflict",
109
        '410' => "HTTP/1.1 410 Gone",
110
        '411' => "HTTP/1.1 411 Length Required",
111
        '412' => "HTTP/1.1 412 Precondition Failed",
112
        '413' => "HTTP/1.1 413 Request Entity Too Large",
113
        '414' => "HTTP/1.1 414 Request-URI Too Long",
114
        '415' => "HTTP/1.1 415 Unsupported Media Type",
115
        '416' => "HTTP/1.1 416 Requested Range Not Satisfiable",
116
        '417' => "HTTP/1.1 417 Expectation Failed",
117
        '500' => "HTTP/1.1 500 Internal Server Error",
118
        '501' => "HTTP/1.1 501 Not Implemented",
119
        '502' => "HTTP/1.1 502 Bad Gateway",
120
        '503' => "HTTP/1.1 503 Service Unavailable",
121
        '504' => "HTTP/1.1 504 Gateway Timeout",
122
        '505' => "HTTP/1.1 505 HTTP Version Not Supported",
123
    );
124
    
125
    /**
126
        * @var        array The array with the HTTP status codes to be used here.
127
        */
128
    protected $httpStatusCodes = null;
129
    
130
    /**
131
     * @var        string The HTTP status code to send for the response.
132
     */
133
    protected $httpStatusCode = '200';
134
    
135
    /**
136
     * @var        array The HTTP headers scheduled to be sent with the response.
137
     */
138
    protected $httpHeaders = array();
139
    
140
    /**
141
     * @var        array The Cookies scheduled to be sent with the response.
142
     */
143
    protected $cookies = array();
144
    
145
    /**
146
     * @var        array An array of redirect information, or null if no redirect.
147
     */
148
    protected $redirect = null;
149
    
150
    /**
151
     * Initialize this Response.
152
     *
153
     * @param      Context $context A Context instance.
154
     * @param      array   $parameters An array of initialization parameters.
155
     *
156
     * @author     David Zülke <[email protected]>
157
     * @since      0.11.0
158
     */
159
    public function initialize(Context $context, array $parameters = array())
160
    {
161
        parent::initialize($context, $parameters);
162
163
        /** @var WebRequest $request */
164
        $request = $context->getRequest();
165
        
166
        // if 'cookie_secure' is set, and null, then we need to set whatever WebRequest::isHttps() returns
167
        if (array_key_exists('cookie_secure', $parameters) && $parameters['cookie_secure'] === null) {
168
            $parameters['cookie_secure'] = $request->isHttps();
169
        }
170
        
171
        $this->setParameters(array(
172
            'cookie_lifetime' => isset($parameters['cookie_lifetime']) ? $parameters['cookie_lifetime'] : 0,
173
            'cookie_path'     => isset($parameters['cookie_path'])     ? $parameters['cookie_path']     : null,
174
            'cookie_domain'   => isset($parameters['cookie_domain'])   ? $parameters['cookie_domain']   : "",
175
            'cookie_secure'   => isset($parameters['cookie_secure'])   ? $parameters['cookie_secure']   : false,
176
            'cookie_httponly' => isset($parameters['cookie_httponly']) ? $parameters['cookie_httponly'] : false,
177
            // For historical reasons, PHP's setcookie() encodes cookies with urlencode(), which
178
            // is not compliant with RFC 6265 as it encodes spaces as a "+" sign instead of "%20".
179
            // This makes most client-side Javascript cookie libraries decode it not as a space
180
            // but as an actual plus sign. We sadly cannot change the default encoding of cookies
181
            // as it would be a breaking change, but introduced a setting instead, which we
182
            // recommend to set to "rawurlencode" for new projects.
183
            'cookie_encode_callback' => isset($parameters['cookie_encode_callback']) ? $parameters['cookie_encode_callback'] : 'urlencode',
184
        ));
185
        
186
        switch ($request->getProtocol()) {
187
            case 'HTTP/1.1':
188
                $this->httpStatusCodes = $this->http11StatusCodes;
189
                break;
190
            default:
191
                $this->httpStatusCodes = $this->http10StatusCodes;
192
        }
193
    }
194
    
195
    /**
196
     * Send all response data to the client.
197
     *
198
     * @param      OutputType $outputType An optional Output Type object with information
199
     *                             the response can use to send additional data,
200
     *                             such as HTTP headers
201
     *
202
     * @author     David Zülke <[email protected]>
203
     * @since      0.11.0
204
     */
205
    public function send(OutputType $outputType = null)
206
    {
207
        if ($this->redirect) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->redirect of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
208
            $location = $this->redirect['location'];
209
            if (!preg_match('#^[^:]+://#', $location)) {
210
                if (isset($location[0]) && $location[0] == '/') {
211
                    /** @var WebRequest $rq */
212
                    $rq = $this->context->getRequest();
213
                    $location = $rq->getUrlScheme() . '://' . $rq->getUrlAuthority() . $location;
214
                } else {
215
                    $location = $this->context->getRouting()->getBaseHref() . $location;
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Agavi\Routing\Routing as the method getBaseHref() does only exist in the following sub-classes of Agavi\Routing\Routing: Agavi\Routing\WebRouting. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
216
                }
217
            }
218
            $this->setHttpHeader('Location', $location);
219
            $this->setHttpStatusCode($this->redirect['code']);
220
            if ($this->getParameter('send_content_length', true) && !$this->hasHttpHeader('Content-Length') && !$this->getParameter('send_redirect_content', false)) {
221
                $this->setHttpHeader('Content-Length', 0);
222
            }
223
        }
224
        $this->sendHttpResponseHeaders($outputType);
225
        if (!$this->redirect || $this->getParameter('send_redirect_content', false)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->redirect of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
226
            $this->sendContent();
227
        }
228
    }
229
    
230
    /**
231
     * Send the content for this response
232
     *
233
     * @author     David Zülke <[email protected]>
234
     * @since      0.11.0
235
     */
236
    public function sendContent()
237
    {
238
        if (is_resource($this->content) && $this->getParameter('use_sendfile_header', false)) {
239
            $info = stream_get_meta_data($this->content);
240
            if ($info['wrapper_type'] == 'plainfile') {
241
                header($this->getParameter('sendfile_header_name', 'X-Sendfile') . ': ' . $info['uri']);
242
                return;
243
            }
244
        }
245
        return parent::sendContent();
246
    }
247
    
248
    /**
249
     * Clear all response data.
250
     *
251
     * @author     David Zülke <[email protected]>
252
     * @since      0.11.0
253
     */
254
    public function clear()
255
    {
256
        $this->clearContent();
257
        $this->httpStatusCode = '200';
258
        $this->httpHeaders = array();
259
        $this->cookies = array();
260
        $this->redirect = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $redirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
261
    }
262
    
263
    /**
264
     * Check whether or not some content is set.
265
     *
266
     * @return     bool If any content is set, false otherwise.
267
     *
268
     * @author     David Zülke <[email protected]>
269
     * @since      0.11.6
270
     */
271
    public function hasContent()
272
    {
273
        return $this->content !== null && $this->content !== '';
274
    }
275
    
276
    /**
277
     * Set the content type for the response.
278
     *
279
     * @param      string $type A content type.
280
     *
281
     * @author     David Zülke <[email protected]>
282
     * @since      0.9.0
283
     */
284
    public function setContentType($type)
285
    {
286
        $this->setHttpHeader('Content-Type', $type);
287
    }
288
    
289
    /**
290
     * Retrieve the content type set for the response.
291
     *
292
     * @return     string A content type, or null if none is set.
293
     *
294
     * @author     David Zülke <[email protected]>
295
     * @since      0.9.0
296
     */
297
    public function getContentType()
298
    {
299
        $retval = $this->getHttpHeader('Content-Type');
300
        if (is_array($retval) && count($retval)) {
301
            return $retval[0];
302
        } else {
303
            return null;
304
        }
305
    }
306
    
307
    /**
308
     * Import response metadata (headers, cookies) from another response.
309
     *
310
     * @param      Response $otherResponse The other response to import information from.
311
     *
312
     * @author     David Zülke <[email protected]>
313
     * @since      0.11.0
314
     */
315
    public function merge(Response $otherResponse)
316
    {
317
        parent::merge($otherResponse);
318
        
319
        if ($otherResponse instanceof WebResponse) {
320
            foreach ($otherResponse->getHttpHeaders() as $name => $value) {
321
                if (!$this->hasHttpHeader($name)) {
322
                    $this->setHttpHeader($name, $value);
323
                }
324
            }
325
            foreach ($otherResponse->getCookies() as $name => $cookie) {
326
                if (!$this->hasCookie($name)) {
327
                    $this->setCookie($name, $cookie['value'], $cookie['lifetime'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httponly'], $cookie['encode_callback']);
328
                }
329
            }
330
            if ($otherResponse->hasRedirect() && !$this->hasRedirect()) {
331
                $redirect = $otherResponse->getRedirect();
332
                $this->setRedirect($redirect['location'], $redirect['code']);
333
            }
334
        }
335
    }
336
    
337
    /**
338
     * Check if the given HTTP status code is valid.
339
     *
340
     * @param      string $code A numeric HTTP status code.
341
     *
342
     * @return     bool True, if the code is valid, or false otherwise.
343
     *
344
     * @author     David Zülke <[email protected]>
345
     * @since      0.11.3
346
     */
347
    public function validateHttpStatusCode($code)
348
    {
349
        $code = (string)$code;
350
        return isset($this->httpStatusCodes[$code]);
351
    }
352
    
353
    /**
354
     * Sets a HTTP status code for the response.
355
     *
356
     * @param      string $code A numeric HTTP status code.
357
     *
358
     * @author     David Zülke <[email protected]>
359
     * @since      0.11.0
360
     */
361
    public function setHttpStatusCode($code)
362
    {
363
        $code = (string)$code;
364 View Code Duplication
        if ($this->validateHttpStatusCode($code)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
365
            $this->httpStatusCode = $code;
366
        } else {
367
            throw new AgaviException(sprintf('Invalid %s Status code: %s', $this->context->getRequest()->getProtocol(), $code));
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Agavi\Request\Request as the method getProtocol() does only exist in the following sub-classes of Agavi\Request\Request: Agavi\Request\SecureWebRequest, Agavi\Request\WebRequest. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
368
        }
369
    }
370
    
371
    /**
372
     * Gets the HTTP status code set for the response.
373
     *
374
     * @return     string A numeric HTTP status code between 100 and 505, or null
375
     *                    if no status code has been set.
376
     *
377
     * @author     David Zülke <[email protected]>
378
     * @since      0.11.0
379
     */
380
    public function getHttpStatusCode()
381
    {
382
        return $this->httpStatusCode;
383
    }
384
385
    /**
386
     * Normalizes a HTTP header names
387
     *
388
     * @param      string $name A HTTP header name
389
     *
390
     * @return     string A normalized HTTP header name
391
     *
392
     * @author     David Zülke <[email protected]>
393
     * @since      0.11.0
394
     */
395
    public function normalizeHttpHeaderName($name)
396
    {
397
        if (strtolower($name) == "etag") {
398
            return "ETag";
399
        } elseif (strtolower($name) == "www-authenticate") {
400
            return "WWW-Authenticate";
401
        } else {
402
            return str_replace(' ', '-', ucwords(str_replace('-', ' ', strtolower($name))));
403
        }
404
    }
405
406
    /**
407
     * Retrieve the HTTP header values set for the response.
408
     *
409
     * @param      string $name A HTTP header field name.
410
     *
411
     * @return     array All values set for that header, or null if no headers set
412
     *
413
     * @author     David Zülke <[email protected]>
414
     * @since      0.11.0
415
     */
416 View Code Duplication
    public function getHttpHeader($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
417
    {
418
        $name = $this->normalizeHttpHeaderName($name);
419
        $retval = null;
420
        if (isset($this->httpHeaders[$name])) {
421
            $retval = $this->httpHeaders[$name];
422
        }
423
        return $retval;
424
    }
425
426
    /**
427
     * Retrieve the HTTP headers set for the response.
428
     *
429
     * @return     array An associative array of HTTP header names and values.
430
     *
431
     * @author     David Zülke <[email protected]>
432
     * @since      0.11.0
433
     */
434
    public function getHttpHeaders()
435
    {
436
        return $this->httpHeaders;
437
    }
438
439
    /**
440
     * Check if an HTTP header has been set for the response.
441
     *
442
     * @param      string $name A HTTP header field name.
443
     *
444
     * @return     bool true if the header exists, false otherwise.
445
     *
446
     * @author     David Zülke <[email protected]>
447
     * @since      0.11.0
448
     */
449 View Code Duplication
    public function hasHttpHeader($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
450
    {
451
        $name = $this->normalizeHttpHeaderName($name);
452
        $retval = false;
453
        if (isset($this->httpHeaders[$name])) {
454
            $retval = true;
455
        }
456
        return $retval;
457
    }
458
459
    /**
460
     * Set a HTTP header for the response
461
     *
462
     * @param      string $name A HTTP header field name.
463
     * @param      mixed  $value A HTTP header field value, of an array of values.
464
     * @param      bool   $replace If true, a header with that name will be overwritten,
465
     *                    otherwise, the value will be appended.
466
     *
467
     * @author     David Zülke <[email protected]>
468
     * @since      0.11.0
469
     */
470
    public function setHttpHeader($name, $value, $replace = true)
471
    {
472
        $name = $this->normalizeHttpHeaderName($name);
473
        if (!isset($this->httpHeaders[$name]) || $replace) {
474
            $this->httpHeaders[$name] = array();
475
        }
476
        if (is_array($value)) {
477
            $this->httpHeaders[$name] = array_merge($this->httpHeaders[$name], $value);
478
        } else {
479
            $this->httpHeaders[$name][] = $value;
480
        }
481
    }
482
483
    /**
484
     * Send a cookie.
485
     *
486
     * @param      string         $name A cookie name.
487
     * @param      mixed          $value Data to store into a cookie. If null or empty cookie
488
     *                            will be tried to be removed.
489
     * @param      mixed          $lifetime The lifetime of the cookie in seconds. When you pass 0
490
     *                            the cookie will be valid until the browser is closed.
491
     *                            You can also use a strtotime() string instead of an int.
492
     * @param      string         $path The path on the server the cookie will be available on.
493
     * @param      string         $domain The domain the cookie is available on.
494
     * @param      bool           $secure Indicates that the cookie should only be transmitted
495
     *                            over a secure HTTPS connection.
496
     * @param      bool           $httponly Whether the cookie will be made accessible only through
497
     *                            the HTTP protocol, and not to client-side scripts.
498
     * @param      callable|bool  $encodeCallback Callback to encode the cookie value. Set to false
499
     *                            if you did already encode the value on your own.
500
     *
501
     * @throws     AgaviException If $encodeCallback is neither false nor callable.
502
     *
503
     * @author     Veikko Mäkinen <[email protected]>
504
     * @author     David Zülke <[email protected]>
505
     * @since      0.11.0
506
     */
507
    public function setCookie($name, $value, $lifetime = null, $path = null, $domain = null, $secure = null, $httponly = null, $encodeCallback = null)
508
    {
509
        $lifetime       =         $lifetime       !== null ? $lifetime       : $this->getParameter('cookie_lifetime');
510
        $path           =         $path           !== null ? $path           : $this->getParameter('cookie_path');
511
        $domain         =         $domain         !== null ? $domain         : $this->getParameter('cookie_domain');
512
        $secure         = (bool) ($secure         !== null ? $secure         : $this->getParameter('cookie_secure'));
513
        $httponly       = (bool) ($httponly       !== null ? $httponly       : $this->getParameter('cookie_httponly'));
514
        $encodeCallback =         $encodeCallback !== null ? $encodeCallback : $this->getParameter('cookie_encode_callback');
515
        
516
        if ($encodeCallback !== false && !is_callable($encodeCallback)) {
517
            throw new AgaviException(sprintf('setCookie() $encodeCallback argument is not callable: %s', $encodeCallback));
518
        }
519
        
520
        $this->cookies[$name] = array(
521
            'value' => $value,
522
            'lifetime' => $lifetime,
523
            'path' => $path,
524
            'domain' => $domain,
525
            'secure' => $secure,
526
            'httponly' => $httponly,
527
            'encode_callback' => $encodeCallback
528
        );
529
    }
530
    
531
    /**
532
     * Unset an existing cookie.
533
     * All arguments must reflect the values of the cookie that is already set.
534
     *
535
     * @param      string $name A cookie name.
536
     * @param      string $path The path on the server the cookie will be available on.
537
     * @param      string $domain The domain the cookie is available on.
538
     * @param      bool   $secure Indicates that the cookie should only be transmitted
539
     *                    over a secure HTTPS connection.
540
     * @param      bool   $httponly Whether the cookie will be made accessible only through
541
     *                    the HTTP protocol, and not to client-side scripts.
542
     *
543
     * @author     Ross Lawley <[email protected]>
544
     * @author     David Zülke <[email protected]>
545
     * @since      0.11.0
546
     */
547
    public function unsetCookie($name, $path = null, $domain = null, $secure = null, $httponly = null)
548
    {
549
        // false as the value, triggers deletion
550
        // null for the lifetime, since Agavi automatically sets that when the value is false or null
551
        $this->setCookie($name, false, null, $path, $domain, $secure, $httponly);
552
    }
553
    
554
    /**
555
     * Get a cookie set for later sending.
556
     *
557
     * @param      string $name The name of the cookie.
558
     *
559
     * @return     array An associative array containing the cookie data or null
560
     *                   if no cookie with that name has been set.
561
     *
562
     * @author     David Zülke <[email protected]>
563
     * @since      0.11.0
564
     */
565
    public function getCookie($name)
566
    {
567
        if (isset($this->cookies[$name])) {
568
            return $this->cookies[$name];
569
        }
570
    }
571
    
572
    /**
573
     * Check if a cookie has been set for later sending.
574
     *
575
     * @param      string $name The name of the cookie.
576
     *
577
     * @return     bool True if a cookie with that name has been set, else false.
578
     *
579
     * @author     David Zülke <[email protected]>
580
     * @since      0.11.0
581
     */
582
    public function hasCookie($name)
583
    {
584
        return isset($this->cookies[$name]);
585
    }
586
    
587
    /**
588
     * Remove a cookie previously set for later sending.
589
     *
590
     * This method cannot be used to unset a cookie. It's purpose is to remove a
591
     * cookie from the list of cookies to be sent along with the response. If you
592
     * wish to remove an existing cookie, use the setCookie method and supply null
593
     * as the value.
594
     *
595
     * @param      string $name The name of the cookie.
596
     *
597
     * @author     David Zülke <[email protected]>
598
     * @since      0.11.0
599
     */
600
    public function removeCookie($name)
601
    {
602
        if (isset($this->cookies[$name])) {
603
            unset($this->cookies[$name]);
604
        }
605
    }
606
    
607
    /**
608
     * Get a list of cookies set for later sending.
609
     *
610
     * @return     array An associative array of cookie names (key) and cookie
611
     *                   information (value, associative array).
612
     *
613
     * @author     David Zülke <[email protected]>
614
     * @since      0.11.0
615
     */
616
    public function getCookies()
617
    {
618
        return $this->cookies;
619
    }
620
    
621
    /**
622
     * Remove the HTTP header set for the response
623
     *
624
     * @param      string $name A HTTP header field name.
625
     *
626
     * @return     mixed The removed header's value or null if header was not set.
627
     *
628
     * @author     David Zülke <[email protected]>
629
     * @since      0.11.0
630
     */
631 View Code Duplication
    public function removeHttpHeader($name)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
632
    {
633
        $name = $this->normalizeHttpHeaderName($name);
634
        $retval = null;
635
        if (isset($this->httpHeaders[$name])) {
636
            $retval = $this->httpHeaders[$name];
637
            unset($this->httpHeaders[$name]);
638
        }
639
        return $retval;
640
    }
641
642
    /**
643
     * Clears the HTTP headers set for this response.
644
     *
645
     * @author     David Zülke <[email protected]>
646
     * @since      0.11.0
647
     */
648
    public function clearHttpHeaders()
649
    {
650
        $this->httpHeaders = array();
651
    }
652
    
653
    /**
654
     * Sends HTTP Status code, headers and cookies
655
     *
656
     * @author     David Zülke <[email protected]>
657
     * @since      0.11.0
658
     */
659
    protected function sendHttpResponseHeaders(OutputType $outputType = null)
660
    {
661
        if ($outputType === null) {
662
            $outputType = $this->getOutputType();
663
        }
664
        
665
        // send HTTP status code
666
        if (isset($this->httpStatusCode) && isset($this->httpStatusCodes[$this->httpStatusCode])) {
667
            header($this->httpStatusCodes[$this->httpStatusCode]);
668
        }
669
        
670
        if ($outputType !== null) {
671
            $httpHeaders = $outputType->getParameter('http_headers');
672
            if (!is_array($httpHeaders)) {
673
                $httpHeaders = array();
674
            }
675
            foreach ($httpHeaders as $name => $value) {
676
                if (!$this->hasHttpHeader($name)) {
677
                    $this->setHttpHeader($name, $value);
678
                }
679
            }
680
        }
681
        
682
        if ($this->getParameter('send_content_length', true) && !$this->hasHttpHeader('Content-Length') && ($contentSize = $this->getContentSize()) !== false) {
683
            $this->setHttpHeader('Content-Length', $contentSize);
684
        }
685
        
686
        if ($this->getParameter('expose_agavi', true) && !$this->hasHttpHeader('X-Powered-By')) {
687
            if (Config::get('expose_agavi_version', $expose_php = ini_get('expose_php'))) {
688
                $xpbh = Config::get('agavi.release');
689
            } else {
690
                $xpbh = Config::get('agavi.name');
691
            }
692
            if ($expose_php) {
693
                $xpbh .= ' on PHP/' . PHP_VERSION;
694
            }
695
            $this->setHttpHeader('X-Powered-By', $xpbh);
696
        }
697
        
698
        $routing = $this->context->getRouting();
699
        if ($routing instanceof WebRouting) {
700
            $basePath = $routing->getBasePath();
701
        } else {
702
            $basePath = '/';
703
        }
704
        
705
        // send cookies
706
        foreach ($this->cookies as $name => $values) {
707
            if (is_string($values['lifetime'])) {
708
                // a string, so we pass it to strtotime()
709
                $expire = strtotime($values['lifetime']);
710
            } else {
711
                // do we want to set expiration time or not?
712
                $expire = ($values['lifetime'] != 0) ? time() + $values['lifetime'] : 0;
713
            }
714
715
            if ($values['value'] === false || $values['value'] === null || $values['value'] === '') {
716
                $expire = time() - 3600 * 24;
717
            }
718
719
            if ($values['encode_callback']) {
720
                $values['value'] = call_user_func($values['encode_callback'], $values['value']);
721
            }
722
            
723
            if ($values['path'] === null) {
724
                $values['path'] = $basePath;
725
            }
726
            
727
            setrawcookie($name, $values['value'], $expire, $values['path'], $values['domain'], $values['secure'], $values['httponly']);
728
        }
729
        
730
        // send headers
731
        foreach ($this->httpHeaders as $name => $values) {
732
            foreach ($values as $key => $value) {
733
                if ($key == 0) {
734
                    header($name . ': ' . $value);
735
                } else {
736
                    header($name . ': ' . $value, false);
737
                }
738
            }
739
        }
740
    }
741
742
    /**
743
     * Redirect externally.
744
     *
745
     * @param      mixed $location Where to redirect.
746
     * @param      int   $code
747
     *
748
     * @author     David Zülke <[email protected]>
749
     * @since      0.11.0
750
     */
751
    public function setRedirect($location, $code = 302)
752
    {
753 View Code Duplication
        if (!$this->validateHttpStatusCode($code)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
754
            throw new AgaviException(sprintf('Invalid %s Redirect Status code: %s', $this->context->getRequest()->getProtocol(), $code));
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Agavi\Request\Request as the method getProtocol() does only exist in the following sub-classes of Agavi\Request\Request: Agavi\Request\SecureWebRequest, Agavi\Request\WebRequest. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
755
        }
756
        $this->redirect = array('location' => $location, 'code' => $code);
757
    }
758
759
    /**
760
     * Get info about the set redirect.
761
     *
762
     * @return     array An assoc array of redirect info, or null if none set.
763
     *
764
     * @author     David Zülke <[email protected]>
765
     * @since      0.11.0
766
     */
767
    public function getRedirect()
768
    {
769
        return $this->redirect;
770
    }
771
772
    /**
773
     * Check if a redirect is set.
774
     *
775
     * @return     bool true, if a redirect is set, otherwise false
776
     *
777
     * @author     David Zülke <[email protected]>
778
     * @since      0.11.0
779
     */
780
    public function hasRedirect()
781
    {
782
        return $this->redirect !== null;
783
    }
784
785
    /**
786
     * Clear any set redirect information.
787
     *
788
     * @author     David Zülke <[email protected]>
789
     * @since      0.11.0
790
     */
791
    public function clearRedirect()
792
    {
793
        $this->redirect = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $redirect.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
794
    }
795
}
796