* @license GNU General Public License, version 2 (GPL-2.0) * * For full copyright and license information, please see * the docs/CREDITS.txt file. * */ namespace phpbb\storage\controller; use phpbb\cache\service; use phpbb\db\driver\driver_interface; use phpbb\exception\http_exception; use phpbb\storage\exception\exception; use phpbb\storage\storage; use Symfony\Component\HttpFoundation\Request as symfony_request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\StreamedResponse; /** * Generic controller for storage */ class controller { /** @var service */ protected $cache; /** @var driver_interface */ protected $db; /** @var storage */ protected $storage; /** @var symfony_request */ protected $symfony_request; /** * Constructor * * @param service $cache * @param driver_interface $db * @param storage $storage * @param symfony_request $symfony_request */ public function __construct(service $cache, driver_interface $db, storage $storage, symfony_request $symfony_request) { $this->cache = $cache; $this->db = $db; $this->storage = $storage; $this->symfony_request = $symfony_request; } /** * Handler * * @param string $file File path * * @return Response a Symfony response object * * @throws http_exception when can't access $file * @throws exception when there is an error reading the file */ public function handle(string $file): Response { $response = new StreamedResponse(); if (!static::is_allowed($file)) { throw new http_exception(403, 'Forbidden'); } if (!static::file_exists($file)) { throw new http_exception(404, 'Not Found'); } static::prepare($response, $file); if (headers_sent()) { throw new http_exception(500, 'Headers already sent'); } return $response; } /** * If the user is allowed to download the file * * @param string $file File path * * @return bool */ protected function is_allowed(string $file): bool { return true; } /** * Check if file exists * * @param string $file File path * * @return bool */ protected function file_exists(string $file): bool { return $this->storage->exists($file); } /** * Prepare response * * @param StreamedResponse $response * @param string $file File path * * @return void * @throws exception when there is an error reading the file */ protected function prepare(StreamedResponse $response, string $file): void { $file_info = $this->storage->file_info($file); // Add Content-Type header if (!$response->headers->has('Content-Type')) { try { $content_type = $file_info->get('mimetype'); } catch (exception $e) { $content_type = 'application/octet-stream'; } $response->headers->set('Content-Type', $content_type); } // Add Content-Length header if we have the file size if (!$response->headers->has('Content-Length')) { try { $response->headers->set('Content-Length', $file_info->get('size')); } catch (exception $e) { // Just don't send this header } } @set_time_limit(0); $fp = $this->storage->read_stream($file); // Close db connection $this->file_gc(); $output = fopen('php://output', 'w+b'); $response->setCallback(function () use ($fp, $output) { stream_copy_to_stream($fp, $output); fclose($fp); fclose($output); flush(); // Terminate script to avoid the execution of terminate events // This avoid possible errors with db connection closed exit; }); $response->isNotModified($this->symfony_request); } /** * Garbage Collection */ protected function file_gc(): void { $this->cache->unload(); // Equivalent to $this->cache->get_driver()->unload(); $this->db->sql_close(); } }