• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 11878724576

17 Nov 2024 12:12PM UTC coverage: 84.428% (+0.001%) from 84.427%
11878724576

push

github

web-flow
fix: code issues after merging develop (#9284)

* cs-fix

* fix services call

* apply rector rules

3 of 3 new or added lines in 2 files covered. (100.0%)

361 existing lines in 20 files now uncovered.

20597 of 24396 relevant lines covered (84.43%)

189.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.4
/system/HTTP/DownloadResponse.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\DownloadException;
17
use CodeIgniter\Files\File;
18
use Config\App;
19
use Config\Mimes;
20

21
/**
22
 * HTTP response when a download is requested.
23
 *
24
 * @see \CodeIgniter\HTTP\DownloadResponseTest
25
 */
26
class DownloadResponse extends Response
27
{
28
    /**
29
     * Download file name
30
     */
31
    private string $filename;
32

33
    /**
34
     * Download for file
35
     */
36
    private ?File $file = null;
37

38
    /**
39
     * mime set flag
40
     */
41
    private readonly bool $setMime;
42

43
    /**
44
     * Download for binary
45
     */
46
    private ?string $binary = null;
47

48
    /**
49
     * Download charset
50
     */
51
    private string $charset = 'UTF-8';
52

53
    /**
54
     * Download reason
55
     *
56
     * @var string
57
     */
58
    protected $reason = 'OK';
59

60
    /**
61
     * The current status code for this response.
62
     *
63
     * @var int
64
     */
65
    protected $statusCode = 200;
66

67
    /**
68
     * Constructor.
69
     */
70
    public function __construct(string $filename, bool $setMime)
71
    {
72
        parent::__construct(config(App::class));
31✔
73

74
        $this->filename = $filename;
31✔
75
        $this->setMime  = $setMime;
31✔
76

77
        // Make sure the content type is either specified or detected
78
        $this->removeHeader('Content-Type');
31✔
79
    }
80

81
    /**
82
     * set download for binary string.
83
     *
84
     * @return void
85
     */
86
    public function setBinary(string $binary)
87
    {
88
        if ($this->file instanceof File) {
8✔
89
            throw DownloadException::forCannotSetBinary();
1✔
90
        }
91

92
        $this->binary = $binary;
7✔
93
    }
94

95
    /**
96
     * set download for file.
97
     *
98
     * @return void
99
     */
100
    public function setFilePath(string $filepath)
101
    {
102
        if ($this->binary !== null) {
12✔
103
            throw DownloadException::forCannotSetFilePath($filepath);
1✔
104
        }
105

106
        $this->file = new File($filepath, true);
11✔
107
    }
108

109
    /**
110
     * set name for the download.
111
     *
112
     * @return $this
113
     */
114
    public function setFileName(string $filename)
115
    {
116
        $this->filename = $filename;
1✔
117

118
        return $this;
1✔
119
    }
120

121
    /**
122
     * get content length.
123
     */
124
    public function getContentLength(): int
125
    {
126
        if (is_string($this->binary)) {
15✔
127
            return strlen($this->binary);
5✔
128
        }
129

130
        if ($this->file instanceof File) {
11✔
131
            return $this->file->getSize();
8✔
132
        }
133

134
        return 0;
4✔
135
    }
136

137
    /**
138
     * Set content type by guessing mime type from file extension
139
     */
140
    private function setContentTypeByMimeType(): void
141
    {
142
        $mime    = null;
13✔
143
        $charset = '';
13✔
144

145
        if ($this->setMime && ($lastDotPosition = strrpos($this->filename, '.')) !== false) {
13✔
146
            $mime    = Mimes::guessTypeFromExtension(substr($this->filename, $lastDotPosition + 1));
5✔
147
            $charset = $this->charset;
5✔
148
        }
149

150
        if (! is_string($mime)) {
13✔
151
            // Set the default MIME type to send
152
            $mime    = 'application/octet-stream';
8✔
153
            $charset = '';
8✔
154
        }
155

156
        $this->setContentType($mime, $charset);
13✔
157
    }
158

159
    /**
160
     * get download filename.
161
     */
162
    private function getDownloadFileName(): string
163
    {
164
        $filename  = $this->filename;
13✔
165
        $x         = explode('.', $this->filename);
13✔
166
        $extension = end($x);
13✔
167

168
        /* It was reported that browsers on Android 2.1 (and possibly older as well)
169
         * need to have the filename extension upper-cased in order to be able to
170
         * download it.
171
         *
172
         * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
173
         */
174
        // @todo: depend super global
175
        if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT'])
13✔
176
                && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT'])) {
13✔
177
            $x[count($x) - 1] = strtoupper($extension);
1✔
178
            $filename         = implode('.', $x);
1✔
179
        }
180

181
        return $filename;
13✔
182
    }
183

184
    /**
185
     * get Content-Disposition Header string.
186
     */
187
    private function getContentDisposition(): string
188
    {
189
        $downloadFilename = $this->getDownloadFileName();
13✔
190

191
        $utf8Filename = $downloadFilename;
13✔
192

193
        if (strtoupper($this->charset) !== 'UTF-8') {
13✔
194
            $utf8Filename = mb_convert_encoding($downloadFilename, 'UTF-8', $this->charset);
1✔
195
        }
196

197
        $result = sprintf('attachment; filename="%s"', $downloadFilename);
13✔
198

199
        if ($utf8Filename !== '') {
13✔
200
            $result .= '; filename*=UTF-8\'\'' . rawurlencode($utf8Filename);
13✔
201
        }
202

203
        return $result;
13✔
204
    }
205

206
    /**
207
     * Disallows status changing.
208
     *
209
     * @throws DownloadException
210
     */
211
    public function setStatusCode(int $code, string $reason = '')
212
    {
213
        throw DownloadException::forCannotSetStatusCode($code, $reason);
1✔
214
    }
215

216
    /**
217
     * Sets the Content Type header for this response with the mime type
218
     * and, optionally, the charset.
219
     *
220
     * @return ResponseInterface
221
     */
222
    public function setContentType(string $mime, string $charset = 'UTF-8')
223
    {
224
        parent::setContentType($mime, $charset);
31✔
225

226
        if ($charset !== '') {
31✔
227
            $this->charset = $charset;
31✔
228
        }
229

230
        return $this;
31✔
231
    }
232

233
    /**
234
     * Sets the appropriate headers to ensure this response
235
     * is not cached by the browsers.
236
     */
237
    public function noCache(): self
238
    {
239
        $this->removeHeader('Cache-Control');
31✔
240
        $this->setHeader('Cache-Control', ['private', 'no-transform', 'no-store', 'must-revalidate']);
31✔
241

242
        return $this;
31✔
243
    }
244

245
    /**
246
     * {@inheritDoc}
247
     *
248
     * @return $this
249
     *
250
     * @todo Do downloads need CSP or Cookies? Compare with ResponseTrait::send()
251
     */
252
    public function send()
253
    {
254
        // Turn off output buffering completely, even if php.ini output_buffering is not off
255
        if (ENVIRONMENT !== 'testing') {
3✔
UNCOV
256
            while (ob_get_level() > 0) {
×
UNCOV
257
                ob_end_clean();
×
258
            }
259
        }
260

261
        $this->buildHeaders();
3✔
262
        $this->sendHeaders();
3✔
263
        $this->sendBody();
3✔
264

265
        return $this;
3✔
266
    }
267

268
    /**
269
     * set header for file download.
270
     *
271
     * @return void
272
     */
273
    public function buildHeaders()
274
    {
275
        if (! $this->hasHeader('Content-Type')) {
14✔
276
            $this->setContentTypeByMimeType();
13✔
277
        }
278

279
        if (! $this->hasHeader('Content-Disposition')) {
14✔
280
            $this->setHeader('Content-Disposition', $this->getContentDisposition());
13✔
281
        }
282

283
        $this->setHeader('Content-Transfer-Encoding', 'binary');
14✔
284
        $this->setHeader('Content-Length', (string) $this->getContentLength());
14✔
285
    }
286

287
    /**
288
     * output download file text.
289
     *
290
     * @return DownloadResponse
291
     *
292
     * @throws DownloadException
293
     */
294
    public function sendBody()
295
    {
296
        if ($this->binary !== null) {
8✔
297
            return $this->sendBodyByBinary();
3✔
298
        }
299

300
        if ($this->file instanceof File) {
5✔
301
            return $this->sendBodyByFilePath();
4✔
302
        }
303

304
        throw DownloadException::forNotFoundDownloadSource();
1✔
305
    }
306

307
    /**
308
     * output download text by file.
309
     *
310
     * @return DownloadResponse
311
     */
312
    private function sendBodyByFilePath()
313
    {
314
        $splFileObject = $this->file->openFile('rb');
4✔
315

316
        // Flush 1MB chunks of data
317
        while (! $splFileObject->eof() && ($data = $splFileObject->fread(1_048_576)) !== false) {
4✔
318
            echo $data;
4✔
319
            unset($data);
4✔
320
        }
321

322
        return $this;
4✔
323
    }
324

325
    /**
326
     * output download text by binary
327
     *
328
     * @return DownloadResponse
329
     */
330
    private function sendBodyByBinary()
331
    {
332
        echo $this->binary;
3✔
333

334
        return $this;
3✔
335
    }
336

337
    /**
338
     * Sets the response header to display the file in the browser.
339
     *
340
     * @return DownloadResponse
341
     */
342
    public function inline()
343
    {
344
        $this->setHeader('Content-Disposition', 'inline');
1✔
345

346
        return $this;
1✔
347
    }
348
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc