Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
controller
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 5
156
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 / 22
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 / 8
0.00% covered (danger)
0.00%
0 / 1
30
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        if (!static::is_allowed($file))
76        {
77            throw new http_exception(403, 'Forbidden');
78        }
79
80        if (!static::file_exists($file))
81        {
82            throw new http_exception(404, 'Not Found');
83        }
84
85        $response = new StreamedResponse();
86        $fp = $this->storage->read($file);
87        $output = fopen('php://output', 'w+b');
88
89        $response->setCallback(function () use ($fp, $output) {
90            stream_copy_to_stream($fp, $output);
91            fclose($fp);
92            fclose($output);
93            flush();
94
95            // Terminate script to avoid the execution of terminate events
96            // This avoids possible errors with db connection closed
97            exit;
98        });
99
100        static::prepare($response, $file);
101
102        if (headers_sent())
103        {
104            throw new http_exception(500, 'Headers already sent');
105        }
106
107        // Close db connection and unload cache
108        $this->cache->unload();
109        $this->db->sql_close();
110
111        $response->isNotModified($this->symfony_request);
112
113        @set_time_limit(0);
114
115        return $response;
116    }
117
118    /**
119     * If the user is allowed to download the file
120     *
121     * @param string $file        File path
122     *
123     * @return bool
124     */
125    protected function is_allowed(string $file): bool
126    {
127        return true;
128    }
129
130    /**
131     * Check if file exists
132     *
133     * @param string $file        File path
134     *
135     * @return bool
136     */
137    protected function file_exists(string $file): bool
138    {
139        return $this->storage->exists($file);
140    }
141
142    /**
143     * Prepare response
144     *
145     * @param Response $response
146     * @param string $file File path
147     *
148     * @return void
149     * @throws storage_exception when there is an error reading the file
150     */
151    protected function prepare(Response $response, string $file): void
152    {
153        // Add Content-Type header
154        if (!$response->headers->has('Content-Type'))
155        {
156            try
157            {
158                $content_type = $this->extension_guesser->guess($file);
159            }
160            catch (storage_exception $e)
161            {
162                $content_type = 'application/octet-stream';
163            }
164
165            $response->headers->set('Content-Type', $content_type);
166        }
167
168        // Add Content-Length header if we have the file size
169        if (!$response->headers->has('Content-Length'))
170        {
171            try
172            {
173                $response->headers->set('Content-Length', $this->storage->file_size($file));
174            }
175            catch (storage_exception $e)
176            {
177                // Just don't send this header
178            }
179        }
180
181    }
182}