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