Issues (2)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Response.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * This file is part of the Borobudur-Http package.
4
 *
5
 * (c) Hexacodelabs <http://hexacodelabs.com>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Borobudur\Http;
12
13
use Borobudur\Http\Exception\InvalidArgumentException;
14
use Borobudur\Http\Header\Accept\AcceptHeader;
15
use Borobudur\Http\Header\CacheControlHeader;
16
use Borobudur\Http\Header\Content\ContentLengthHeader;
17
use Borobudur\Http\Header\Content\ContentTypeHeader;
18
use Borobudur\Http\Header\ExpiresHeader;
19
use Borobudur\Http\Header\PragmaHeader;
20
use Borobudur\Http\Header\TransferEncodingHeader;
21
use DateTime;
22
23
/**
24
 * @author      Iqbal Maulana <[email protected]>
25
 * @created     7/30/15
26
 */
27
class Response extends AbstractResponse
28
{
29
    use HttpStatusTrait;
30
31
    /**
32
     * @const string
33
     */
34
    const HTTP_VERSION_10 = '1.0';
35
    const HTTP_VERSION_11 = '1.1';
36
37
    /**
38
     * @var SetCookieHeaderBag
39
     */
40
    public $cookies;
41
42
    /**
43
     * @var int
44
     */
45
    protected $chunkLength;
46
47
    /**
48
     * @var int
49
     */
50
    protected $chunkDelayResponse;
51
52
    /**
53
     * Constructor.
54
     *
55
     * @param string $content
56
     * @param int    $statusCode
57
     * @param array  $headers
58
     * @param array  $cookies
59
     */
60
    public function __construct($content = '', $statusCode = 200, array $headers = array(), array $cookies = array())
61
    {
62
        $this->headers = new HeaderBag($headers);
63
        $this->cookies = new SetCookieHeaderBag($cookies);
64
        $this->setStatusCode($statusCode);
65
        $this->setContent($content);
66
        $this->setProtocolVersion(Response::HTTP_VERSION_11);
67
    }
68
69
    /**
70
     * Prepare Response base on Request.
71
     *
72
     * @param Request $request
73
     *
74
     * @return $this
75
     */
76
    public function prepare(Request $request)
77
    {
78
        if ($this->isEmpty() || $this->isInformational()) {
79
            $this->setContent(null);
80
            $this->headers->remove('Content-Type');
81
            $this->headers->remove('Content-Length');
82
        } else {
83
            $this->fixContentType($request);
84
            $this->fixContentLength($request);
85
        }
86
87
        // Fix protocol
88
        if ('HTTP/1.1' !== $request->getServerBag()->get('SERVER_PROTOCOL')) {
89
            $this->setProtocolVersion('1.0');
90
        }
91
92
        $this->fixExpire();
93
94
        return $this;
95
    }
96
97
    /**
98
     * Set response as chunked.
99
     *
100
     * @param int $length Content split length.
101
     * @param int $delay  Delay response in microseconds.
102
     *
103
     * @return $this
104
     */
105
    public function setChunkedTransferEncoding($length = 76, $delay = 1000000)
106
    {
107
        $this->headers->set(new TransferEncodingHeader('chunked'), true);
108
        $this->chunkLength = $length;
109
        $this->chunkDelayResponse = $delay;
110
111
        return $this;
112
    }
113
114
    /**
115
     * Set response as not modified.
116
     *
117
     * @return $this
118
     */
119
    public function setNotModified()
120
    {
121
        $this->setStatusCode(HttpStatus::HTTP_NOT_MODIFIED);
122
        $this->setContent(null);
123
124
        foreach (
125
            array(
126
                'Allow',
127
                'Content-Encoding',
128
                'Content-Language',
129
                'Content-Length',
130
                'Content-MD5',
131
                'Content-Type',
132
                'Transfer-Encoding',
133
                'Last-Modified',
134
            ) as $header
135
        ) {
136
            $this->headers->remove($header);
137
        }
138
139
        return $this;
140
    }
141
142
    /**
143
     * Check if response is no cache.
144
     *
145
     * @return bool
146
     */
147
    public function isNoCache()
148
    {
149
        return
150
            !$this->headers->has('Cache-Control')
151
            && !$this->headers->has('ETag')
152
            && !$this->headers->has('Last-Modified')
153
            && !$this->headers->has('Expires');
154
    }
155
156
    /**
157
     * Send response to client.
158
     *
159
     * @return $this
160
     */
161
    public function send()
162
    {
163
        $content = $this->computeContent();
164
        $this->sendHeaders();
165
166
        if ($content) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $content of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
167
            if ($this->isChunked()) {
168
                $this->sendChunkContent($content);
169
            } else {
170
                echo $content;
171
            }
172
173
            if (function_exists('fastcgi_finish_request')) {
174
                fastcgi_finish_request();
175
            } else {
176
                ob_end_flush();
177
            }
178
        }
179
180
        return $this;
181
    }
182
183
    /**
184
     * Send response header.
185
     *
186
     * @return $this
187
     */
188
    public function sendHeaders()
189
    {
190
        // check is headers already sent.
191
        if (headers_sent()) {
192
            return $this;
193
        }
194
195
        $this->computeContentType();
196
        $this->headers->set($this->computeCacheControl(), true);
197
198
        // send status header
199
        header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText), true, $this->statusCode);
200
        // send headers
201
        $this->createHeaders();
202
        // send cookies header
203
        $this->createCookieHeaders();
204
205
        return $this;
206
    }
207
208
    /**
209
     * Check if current response is chunked.
210
     *
211
     * @return bool
212
     */
213
    public function isChunked()
214
    {
215
        return
216
            $this->headers->has('Transfer-Encoding')
217
            && 'chunked' === $this->headers->first('Transfer-Encoding')->getFieldValue();
218
    }
219
220
    /**
221
     * Cast Response as string representation.
222
     *
223
     * @return string
224
     */
225
    public function __toString()
226
    {
227
        $content = $this->computeContent();
228
        $this->computeContentType();
229
        $this->headers->set($this->computeCacheControl(), true);
230
231
        return
232
            sprintf(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)) . "\r\n" .
233
            $this->headers . "\r\n" .
234
            $this->cookies . "\r\n" .
235
            $content;
236
    }
237
238
    /**
239
     * Add charset if content type exist.
240
     */
241
    protected function computeContentType()
242
    {
243
        if ($this->headers->has('Content-Type')) {
244
            /**
245
             * @var ContentTypeHeader $contentType
246
             */
247
            $contentType = $this->headers->first('Content-Type');
248
            $contentType->setParameter('charset', $this->getCharset());
249
250
            $this->headers->set($contentType, true);
251
        }
252
    }
253
254
    /**
255
     * Get computed content with specific encoding.
256
     *
257
     * @return string
258
     */
259
    protected function computeContent()
260
    {
261
        if ($this->isNotModified()) {
262
            $this->setNotModified();
263
264
            return null;
265
        }
266
267
        $encoding = $this->fixEncoding();
268
        $this->headers->set(new ContentLengthHeader($encoding->encode()->getLength()), true);
269
270
        if ($this->isChunked()) {
271
            if ('1.0' === $this->getProtocolVersion()) {
272
                throw new InvalidArgumentException(sprintf(
273
                    'Transfer-Encoding chunked need HTTP Protocol version 1.1, "%s" given.',
274
                    $this->getProtocolVersion()
275
                ));
276
            }
277
278
            return chunk_split($encoding->getContent(), $this->chunkLength);
279
        }
280
281
        return $encoding->getContent();
282
    }
283
284
    /**
285
     * Get calculated cache control header.
286
     *
287
     * @return CacheControlHeader
288
     */
289
    protected function computeCacheControl()
290
    {
291
        if ($this->isNoCache()) {
292
            return (new CacheControlHeader())->setNoCache();
293
        }
294
295
        if (!$this->headers->has('Cache-Control')) {
296
            return (new CacheControlHeader())->setPrivate()->setMustReValidate();
297
        }
298
299
        /**
300
         * @var CacheControlHeader $header
301
         */
302
        $header = $this->headers->first('Cache-Control', new CacheControlHeader());
303
304
        return $header->setAutoPrivilege();
305
    }
306
307
    /**
308
     * Add content type header from request if not defined.
309
     *
310
     * @param Request $request
311
     */
312
    private function fixContentType(Request $request)
313
    {
314
        // Add content type from request if not defined.
315
        if (false === $this->headers->has('Content-Type') && true === $request->getHeaderBag()->has('Accept')) {
316
            /**
317
             * @var AcceptHeader $acceptHeader
318
             */
319
            $acceptHeader = $request->getHeaderBag()->first('Accept');
320
            // get first accept content type=
321
            $this->setContentType($acceptHeader->first()->getValue());
322
        }
323
    }
324
325
    /**
326
     * Fix content length header.
327
     *
328
     * @param Request $request
329
     */
330
    private function fixContentLength(Request $request)
331
    {
332
        if ($this->headers->has('Transfer-Encoding')) {
333
            $this->headers->remove('Content-Length');
334
        }
335
336
        if ($request->isMethod(Request::HTTP_METHOD_HEAD)) {
337
            $contentLengthHeader = $this->headers->first('Content-Length');
338
            $this->setContent(null);
339
            if ($length = $contentLengthHeader->getFieldValue()) {
340
                $this->headers->set(new ContentLengthHeader($length), true);
341
            }
342
        }
343
    }
344
345
    /**
346
     * Send extra expire info if needed.
347
     */
348
    private function fixExpire()
349
    {
350
        if ('1.0' === $this->getProtocolVersion() && $this->headers->has('Cache-Control')) {
351
            /**
352
             * @var CacheControlHeader $cacheControl
353
             */
354
            $cacheControl = $this->headers->first('Cache-Control');
355
            if (true === $cacheControl->isNoCache()) {
356
                $this->headers->set(new PragmaHeader('no-cache'), true);
357
                $this->headers->set(new ExpiresHeader(new DateTime('-1 year')), true);
358
            }
359
        }
360
    }
361
362
    /**
363
     * Fix http encoding header.
364
     */
365
    private function fixEncoding()
366
    {
367
        $encoding = new HttpEncoding($this->content, HttpEncoding::NONE);
368
369
        if ($this->headers->has('Content-Encoding')) {
370
            $contentEncoding = $this->headers->first('Content-Encoding');
371
            $encoding->setEncoding($contentEncoding->getFieldValue());
372
        }
373
374
        return $encoding;
375
    }
376
377
    /**
378
     * Send http headers.
379
     */
380
    private function createHeaders()
381
    {
382
        foreach ($this->headers as $headers) {
383
            foreach ($headers as $header) {
384
                header((string) $header, false, $this->statusCode);
385
            }
386
        }
387
    }
388
389
    /**
390
     * Send http cookie headers.
391
     */
392
    private function createCookieHeaders()
393
    {
394
        foreach ($this->cookies as $cookie) {
395
            header((string) $cookie, false, $this->statusCode);
396
        }
397
    }
398
399
    /**
400
     * Send chunk content.
401
     *
402
     * @param string $content
403
     */
404
    private function sendChunkContent($content)
405
    {
406
        flush();
407
        ob_flush();
408
409
        foreach (explode("\r\n", $content) as $chunk) {
410
            echo sprintf("%x\r\n", strlen($chunk));
411
            echo $chunk . "\r\n";
412
            flush();
413
            ob_flush();
414
            usleep($this->chunkDelayResponse);
415
        }
416
    }
417
}
418