Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
controller
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 6
182
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 is_allowed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 file_exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prepare
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 file_gc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\storage\controller;
15
16use phpbb\cache\service;
17use phpbb\db\driver\driver_interface;
18use phpbb\exception\http_exception;
19use phpbb\mimetype\extension_guesser;
20use phpbb\storage\exception\storage_exception;
21use phpbb\storage\storage;
22use Symfony\Component\HttpFoundation\Request as symfony_request;
23use Symfony\Component\HttpFoundation\Response;
24use Symfony\Component\HttpFoundation\StreamedResponse;
25
26/**
27 * Generic controller for storage
28 */
29class controller
30{
31    /** @var service */
32    protected $cache;
33
34    /** @var driver_interface */
35    protected $db;
36
37    /** @var extension_guesser */
38    protected $extension_guesser;
39
40    /** @var storage */
41    protected $storage;
42
43    /** @var symfony_request */
44    protected $symfony_request;
45
46    /**
47     * Constructor
48     *
49     * @param service                $cache
50     * @param driver_interface        $db
51     * @param storage                $storage
52     * @param symfony_request        $symfony_request
53     */
54    public function __construct(service $cache, driver_interface $db, extension_guesser $extension_guesser, storage $storage, symfony_request $symfony_request)
55    {
56        $this->cache = $cache;
57        $this->db = $db;
58        $this->extension_guesser = $extension_guesser;
59        $this->storage = $storage;
60        $this->symfony_request = $symfony_request;
61    }
62
63    /**
64     * Handler
65     *
66     * @param string $file        File path
67     *
68     * @return Response a Symfony response object
69     *
70     * @throws http_exception when can't access $file
71     * @throws storage_exception when there is an error reading the file
72     */
73    public function handle(string $file): Response
74    {
75        $response = new StreamedResponse();
76
77        if (!static::is_allowed($file))
78        {
79            throw new http_exception(403, 'Forbidden');
80        }
81
82        if (!static::file_exists($file))
83        {
84            throw new http_exception(404, 'Not Found');
85        }
86
87        static::prepare($response, $file);
88
89        if (headers_sent())
90        {
91            throw new http_exception(500, 'Headers already sent');
92        }
93
94        return $response;
95    }
96
97    /**
98     * If the user is allowed to download the file
99     *
100     * @param string $file        File path
101     *
102     * @return bool
103     */
104    protected function is_allowed(string $file): bool
105    {
106        return true;
107    }
108
109    /**
110     * Check if file exists
111     *
112     * @param string $file        File path
113     *
114     * @return bool
115     */
116    protected function file_exists(string $file): bool
117    {
118        return $this->storage->exists($file);
119    }
120
121    /**
122     * Prepare response
123     *
124     * @param StreamedResponse $response
125     * @param string $file File path
126     *
127     * @return void
128     * @throws storage_exception when there is an error reading the file
129     */
130    protected function prepare(StreamedResponse $response, string $file): void
131    {
132        // Add Content-Type header
133        if (!$response->headers->has('Content-Type'))
134        {
135            try
136            {
137                $content_type = $this->extension_guesser->guess($file);
138            }
139            catch (storage_exception $e)
140            {
141                $content_type = 'application/octet-stream';
142            }
143
144            $response->headers->set('Content-Type', $content_type);
145        }
146
147        // Add Content-Length header if we have the file size
148        if (!$response->headers->has('Content-Length'))
149        {
150            try
151            {
152                $response->headers->set('Content-Length', $this->storage->file_size($file));
153            }
154            catch (storage_exception $e)
155            {
156                // Just don't send this header
157            }
158        }
159
160        @set_time_limit(0);
161
162        $fp = $this->storage->read_stream($file);
163
164        // Close db connection
165        $this->file_gc();
166
167        $output = fopen('php://output', 'w+b');
168
169        $response->setCallback(function () use ($fp, $output) {
170            stream_copy_to_stream($fp, $output);
171            fclose($fp);
172            fclose($output);
173            flush();
174
175            // Terminate script to avoid the execution of terminate events
176            // This avoid possible errors with db connection closed
177            exit;
178        });
179
180        $response->isNotModified($this->symfony_request);
181    }
182
183    /**
184    * Garbage Collection
185    */
186    protected function file_gc(): void
187    {
188        $this->cache->unload(); // Equivalent to $this->cache->get_driver()->unload();
189        $this->db->sql_close();
190    }
191}