diff --git a/phpBB/config/default/container/services_storage.yml b/phpBB/config/default/container/services_storage.yml index 9577f52684..b3b118f6e2 100644 --- a/phpBB/config/default/container/services_storage.yml +++ b/phpBB/config/default/container/services_storage.yml @@ -96,6 +96,7 @@ services: - '@auth' - '@cache' - '@config' + - '@content.visibility' - '@dbal.conn' - '@dispatcher' - '@request' diff --git a/phpBB/includes/functions_download.php b/phpBB/includes/functions_download.php index 31164221d1..6ec95e966e 100644 --- a/phpBB/includes/functions_download.php +++ b/phpBB/includes/functions_download.php @@ -43,136 +43,6 @@ function wrap_img_in_html($src, $title) echo ''; } -/** -* Send file to browser -*/ -function send_file_to_browser($attachment, $category) -{ - global $user, $db, $phpbb_dispatcher, $request, $phpbb_container; - - $storage = $phpbb_container->get('storage.attachment'); - - $filename = $attachment['physical_filename']; - - if (!$storage->exists($filename)) - { - send_status_line(404, 'Not Found'); - trigger_error('ERROR_NO_ATTACHMENT'); - } - - // Correct the mime type - we force application/octetstream for all files, except images - // Please do not change this, it is a security precaution - if ($category != ATTACHMENT_CATEGORY_IMAGE || strpos($attachment['mimetype'], 'image') !== 0) - { - $attachment['mimetype'] = (strpos(strtolower($user->browser), 'msie') !== false || strpos(strtolower($user->browser), 'opera') !== false) ? 'application/octetstream' : 'application/octet-stream'; - } - - if (@ob_get_length()) - { - @ob_end_clean(); - } - - // Now send the File Contents to the Browser - try - { - $file_info = $storage->file_info($filename); - $size = $file_info->size; - } - catch (\Exception $e) - { - $size = 0; - } - - /** - * Event to alter attachment before it is sent to browser. - * - * @event core.send_file_to_browser_before - * @var array attachment Attachment data - * @var int category Attachment category - * @var string filename Path to file, including filename - * @var int size File size - * @since 3.1.11-RC1 - */ - $vars = array( - 'attachment', - 'category', - 'filename', - 'size', - ); - extract($phpbb_dispatcher->trigger_event('core.send_file_to_browser_before', compact($vars))); - - // To correctly display further errors we need to make sure we are using the correct headers for both (unsetting content-length may not work) - - // Check if headers already sent or not able to get the file contents. - if (headers_sent()) - { - send_status_line(500, 'Internal Server Error'); - trigger_error('UNABLE_TO_DELIVER_FILE'); - } - - // Make sure the database record for the filesize is correct - if ($size > 0 && $size != $attachment['filesize'] && strpos($attachment['physical_filename'], 'thumb_') === false) - { - // Update database record - $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' - SET filesize = ' . (int) $size . ' - WHERE attach_id = ' . (int) $attachment['attach_id']; - $db->sql_query($sql); - } - - // Now the tricky part... let's dance - header('Cache-Control: private'); - - // Send out the Headers. Do not set Content-Disposition to inline please, it is a security measure for users using the Internet Explorer. - header('Content-Type: ' . $attachment['mimetype']); - - header('X-Content-Type-Options: nosniff'); - - if (empty($user->browser) || ((strpos(strtolower($user->browser), 'msie') !== false) && !phpbb_is_greater_ie_version($user->browser, 7))) - { - header('Content-Disposition: attachment; ' . header_filename(htmlspecialchars_decode($attachment['real_filename'], ENT_COMPAT))); - if (empty($user->browser) || (strpos(strtolower($user->browser), 'msie 6.0') !== false)) - { - header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); - } - } - else - { - header('Content-Disposition: ' . ((strpos($attachment['mimetype'], 'image') === 0) ? 'inline' : 'attachment') . '; ' . header_filename(htmlspecialchars_decode($attachment['real_filename'], ENT_COMPAT))); - if (phpbb_is_greater_ie_version($user->browser, 7) && (strpos($attachment['mimetype'], 'image') !== 0)) - { - header('X-Download-Options: noopen'); - } - } - - if (!set_modified_headers($attachment['filetime'], $user->browser)) - { - if ($size) - { - header("Content-Length: $size"); - } - - // Try to deliver in chunks - @set_time_limit(0); - - $fp = $storage->read_stream($filename); - - // Close the db connection before sending the file etc. - file_gc(false); - - if ($fp !== false) - { - $output = fopen('php://output', 'w+b'); - stream_copy_to_stream($fp, $output); - fclose($fp); - } - - flush(); - } - - exit; -} - /** * Get a browser friendly UTF-8 encoded filename */ @@ -193,149 +63,14 @@ function header_filename($file) return "filename*=UTF-8''" . rawurlencode($file); } -/** -* Check if downloading item is allowed -*/ -function download_allowed() -{ - global $config, $user, $db, $request; - - if (!$config['secure_downloads']) - { - return true; - } - - $url = htmlspecialchars_decode($request->header('Referer'), ENT_COMPAT); - - if (!$url) - { - return ($config['secure_allow_empty_referer']) ? true : false; - } - - // Split URL into domain and script part - $url = @parse_url($url); - - if ($url === false) - { - return ($config['secure_allow_empty_referer']) ? true : false; - } - - $hostname = $url['host']; - unset($url); - - $allowed = ($config['secure_allow_deny']) ? false : true; - $iplist = array(); - - if (($ip_ary = @gethostbynamel($hostname)) !== false) - { - foreach ($ip_ary as $ip) - { - if ($ip) - { - $iplist[] = $ip; - } - } - } - - // Check for own server... - $server_name = $user->host; - - // Forcing server vars is the only way to specify/override the protocol - if ($config['force_server_vars'] || !$server_name) - { - $server_name = $config['server_name']; - } - - if (preg_match('#^.*?' . preg_quote($server_name, '#') . '.*?$#i', $hostname)) - { - $allowed = true; - } - - // Get IP's and Hostnames - if (!$allowed) - { - $sql = 'SELECT site_ip, site_hostname, ip_exclude - FROM ' . SITELIST_TABLE; - $result = $db->sql_query($sql); - - while ($row = $db->sql_fetchrow($result)) - { - $site_ip = trim($row['site_ip']); - $site_hostname = trim($row['site_hostname']); - - if ($site_ip) - { - foreach ($iplist as $ip) - { - if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_ip, '#')) . '$#i', $ip)) - { - if ($row['ip_exclude']) - { - $allowed = ($config['secure_allow_deny']) ? false : true; - break 2; - } - else - { - $allowed = ($config['secure_allow_deny']) ? true : false; - } - } - } - } - - if ($site_hostname) - { - if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_hostname, '#')) . '$#i', $hostname)) - { - if ($row['ip_exclude']) - { - $allowed = ($config['secure_allow_deny']) ? false : true; - break; - } - else - { - $allowed = ($config['secure_allow_deny']) ? true : false; - } - } - } - } - $db->sql_freeresult($result); - } - - return $allowed; -} - -/** -* Check if the browser has the file already and set the appropriate headers- -* @returns false if a resend is in order. -*/ -function set_modified_headers($stamp, $browser) -{ - global $request; - - // let's see if we have to send the file at all - $last_load = $request->header('If-Modified-Since') ? strtotime(trim($request->header('If-Modified-Since'))) : false; - - if ($last_load !== false && $last_load >= $stamp) - { - send_status_line(304, 'Not Modified'); - // seems that we need those too ... browsers - header('Cache-Control: private'); - header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); - return true; - } - else - { - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $stamp) . ' GMT'); - } - return false; -} - /** * Garbage Collection * * @param bool $exit Whether to die or not. * * @return null +* +* @deprecated: 3.3.0-dev (To be removed: 4.0.0) */ function file_gc($exit = true) { @@ -354,287 +89,6 @@ function file_gc($exit = true) } } -/** -* HTTP range support (RFC 2616 Section 14.35) -* -* Allows browsers to request partial file content -* in case a download has been interrupted. -* -* @param int $filesize the size of the file in bytes we are about to deliver -* -* @return mixed false if the whole file has to be delivered -* associative array on success -*/ -function phpbb_http_byte_range($filesize) -{ - // Only call find_range_request() once. - static $request_array; - - if (!$filesize) - { - return false; - } - - if (!isset($request_array)) - { - $request_array = phpbb_find_range_request(); - } - - return (empty($request_array)) ? false : phpbb_parse_range_request($request_array, $filesize); -} - -/** -* Searches for HTTP range request in request headers. -* -* @return mixed false if no request found -* array of strings containing the requested ranges otherwise -* e.g. array(0 => '0-0', 1 => '123-125') -*/ -function phpbb_find_range_request() -{ - global $request; - - $value = $request->header('Range'); - - // Make sure range request starts with "bytes=" - if (strpos($value, 'bytes=') === 0) - { - // Strip leading 'bytes=' - // Multiple ranges can be separated by a comma - return explode(',', substr($value, 6)); - } - - return false; -} - -/** -* Analyses a range request array. -* -* A range request can contain multiple ranges, -* we however only handle the first request and -* only support requests from a given byte to the end of the file. -* -* @param array $request_array array of strings containing the requested ranges -* @param int $filesize the full size of the file in bytes that has been requested -* -* @return mixed false if the whole file has to be delivered -* associative array on success -* byte_pos_start the first byte position, can be passed to fseek() -* byte_pos_end the last byte position -* bytes_requested the number of bytes requested -* bytes_total the full size of the file -*/ -function phpbb_parse_range_request($request_array, $filesize) -{ - $first_byte_pos = -1; - $last_byte_pos = -1; - - // Go through all ranges - foreach ($request_array as $range_string) - { - $range = explode('-', trim($range_string)); - - // "-" is invalid, "0-0" however is valid and means the very first byte. - if (count($range) != 2 || $range[0] === '' && $range[1] === '') - { - continue; - } - - // Substitute defaults - if ($range[0] === '') - { - $range[0] = 0; - } - - if ($range[1] === '') - { - $range[1] = $filesize - 1; - } - - if ($last_byte_pos >= 0 && $last_byte_pos + 1 != $range[0]) - { - // We only support contiguous ranges, no multipart stuff :( - return false; - } - - if ($range[1] && $range[1] < $range[0]) - { - // The requested range contains 0 bytes. - continue; - } - - // Return bytes from $range[0] to $range[1] - if ($first_byte_pos < 0) - { - $first_byte_pos = (int) $range[0]; - } - - $last_byte_pos = (int) $range[1]; - - if ($first_byte_pos >= $filesize) - { - // Requested range not satisfiable - return false; - } - - // Adjust last-byte-pos if it is absent or greater than the content. - if ($range[1] === '' || $last_byte_pos >= $filesize) - { - $last_byte_pos = $filesize - 1; - } - } - - if ($first_byte_pos < 0 || $last_byte_pos < 0) - { - return false; - } - - return array( - 'byte_pos_start' => $first_byte_pos, - 'byte_pos_end' => $last_byte_pos, - 'bytes_requested' => $last_byte_pos - $first_byte_pos + 1, - 'bytes_total' => $filesize, - ); -} - -/** -* Increments the download count of all provided attachments -* -* @param \phpbb\db\driver\driver_interface $db The database object -* @param array|int $ids The attach_id of each attachment -* -* @return null -*/ -function phpbb_increment_downloads($db, $ids) -{ - if (!is_array($ids)) - { - $ids = array($ids); - } - - $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' - SET download_count = download_count + 1 - WHERE ' . $db->sql_in_set('attach_id', $ids); - $db->sql_query($sql); -} - -/** -* Handles authentication when downloading attachments from a post or topic -* -* @param \phpbb\db\driver\driver_interface $db The database object -* @param \phpbb\auth\auth $auth The authentication object -* @param int $topic_id The id of the topic that we are downloading from -* -* @return null -*/ -function phpbb_download_handle_forum_auth($db, $auth, $topic_id) -{ - global $phpbb_container; - - $sql_array = array( - 'SELECT' => 't.topic_visibility, t.forum_id, f.forum_name, f.forum_password, f.parent_id', - 'FROM' => array( - TOPICS_TABLE => 't', - FORUMS_TABLE => 'f', - ), - 'WHERE' => 't.topic_id = ' . (int) $topic_id . ' - AND t.forum_id = f.forum_id', - ); - - $sql = $db->sql_build_query('SELECT', $sql_array); - $result = $db->sql_query($sql); - $row = $db->sql_fetchrow($result); - $db->sql_freeresult($result); - - $phpbb_content_visibility = $phpbb_container->get('content.visibility'); - - if ($row && !$phpbb_content_visibility->is_visible('topic', $row['forum_id'], $row)) - { - send_status_line(404, 'Not Found'); - trigger_error('ERROR_NO_ATTACHMENT'); - } - else if ($row && $auth->acl_get('u_download') && $auth->acl_get('f_download', $row['forum_id'])) - { - if ($row['forum_password']) - { - // Do something else ... ? - login_forum_box($row); - } - } - else - { - send_status_line(403, 'Forbidden'); - trigger_error('SORRY_AUTH_VIEW_ATTACH'); - } -} - -/** -* Handles authentication when downloading attachments from PMs -* -* @param \phpbb\db\driver\driver_interface $db The database object -* @param \phpbb\auth\auth $auth The authentication object -* @param int $user_id The user id -* @param int $msg_id The id of the PM that we are downloading from -* -* @return null -*/ -function phpbb_download_handle_pm_auth($db, $auth, $user_id, $msg_id) -{ - global $phpbb_dispatcher; - - if (!$auth->acl_get('u_pm_download')) - { - send_status_line(403, 'Forbidden'); - trigger_error('SORRY_AUTH_VIEW_ATTACH'); - } - - $allowed = phpbb_download_check_pm_auth($db, $user_id, $msg_id); - - /** - * Event to modify PM attachments download auth - * - * @event core.modify_pm_attach_download_auth - * @var bool allowed Whether the user is allowed to download from that PM or not - * @var int msg_id The id of the PM to download from - * @var int user_id The user id for auth check - * @since 3.1.11-RC1 - */ - $vars = array('allowed', 'msg_id', 'user_id'); - extract($phpbb_dispatcher->trigger_event('core.modify_pm_attach_download_auth', compact($vars))); - - if (!$allowed) - { - send_status_line(403, 'Forbidden'); - trigger_error('ERROR_NO_ATTACHMENT'); - } -} - -/** -* Checks whether a user can download from a particular PM -* -* @param \phpbb\db\driver\driver_interface $db The database object -* @param int $user_id The user id -* @param int $msg_id The id of the PM that we are downloading from -* -* @return bool Whether the user is allowed to download from that PM or not -*/ -function phpbb_download_check_pm_auth($db, $user_id, $msg_id) -{ - // Check if the attachment is within the users scope... - $sql = 'SELECT msg_id - FROM ' . PRIVMSGS_TO_TABLE . ' - WHERE msg_id = ' . (int) $msg_id . ' - AND ( - user_id = ' . (int) $user_id . ' - OR author_id = ' . (int) $user_id . ' - )'; - $result = $db->sql_query_limit($sql, 1); - $allowed = (bool) $db->sql_fetchfield('msg_id'); - $db->sql_freeresult($result); - - return $allowed; -} - /** * Check if the browser is internet explorer version 7+ * diff --git a/phpBB/phpbb/storage/controller/attachment.php b/phpBB/phpbb/storage/controller/attachment.php index 7e93369754..24f638289b 100644 --- a/phpBB/phpbb/storage/controller/attachment.php +++ b/phpBB/phpbb/storage/controller/attachment.php @@ -16,6 +16,7 @@ namespace phpbb\storage\controller; use phpbb\auth\auth; use phpbb\cache\service; use phpbb\config\config; +use phpbb\content_visibility; use phpbb\db\driver\driver_interface; use phpbb\event\dispatcher; use phpbb\request\request; @@ -27,17 +28,17 @@ class attachment extends controller /** @var auth */ protected $auth; - /** @var service */ - protected $cache; - /** @var config */ protected $config; + /** @var content_visibility */ + protected $content_visibility; + /** @var driver_interface */ protected $db; /** @var dispatcher */ - protected $phpbb_dispatcher; + protected $dispatcher; /** @var request */ protected $request; @@ -48,13 +49,14 @@ class attachment extends controller /** @var user */ protected $user; - public function __construct(auth $auth, service $cache, config $config, driver_interface $db, dispatcher $phpbb_dispatcher, request $request, storage $storage, user $user) + public function __construct(auth $auth, service $cache, config $config, $content_visibility, driver_interface $db, dispatcher $dispatcher, request $request, storage $storage, user $user) { $this->auth = $auth; $this->cache = $cache; $this->config = $config; + $this->content_visibility = $content_visibility; $this->db = $db; - $this->phpbb_dispatcher = $phpbb_dispatcher; + $this->dispatcher = $dispatcher; $this->request = $request; $this->storage = $storage; $this->user = $user; @@ -62,20 +64,16 @@ class attachment extends controller public function handle($file) { - global $phpbb_root_path, $phpEx, $phpbb_container; - require($phpbb_root_path . 'includes/functions_download' . '.' . $phpEx); - $attach_id = $file; $mode = $this->request->variable('mode', ''); $thumbnail = $this->request->variable('t', false); + global $phpbb_container; // Start session management, do not update session page. $this->user->session_begin(false); $this->auth->acl($this->user->data); $this->user->setup('viewtopic'); - $phpbb_content_visibility = $phpbb_container->get('content.visibility'); - if (!$this->config['allow_attachments'] && !$this->config['allow_pm_attach']) { send_status_line(404, 'Not Found'); @@ -100,10 +98,10 @@ class attachment extends controller send_status_line(404, 'Not Found'); trigger_error('ERROR_NO_ATTACHMENT'); } - else if (!download_allowed()) + else if (!$this->download_allowed()) { send_status_line(403, 'Forbidden'); - trigger_error($user->lang['LINKAGE_FORBIDDEN']); + trigger_error($this->user->lang['LINKAGE_FORBIDDEN']); } else { @@ -133,7 +131,7 @@ class attachment extends controller { if (!$attachment['in_message']) { - phpbb_download_handle_forum_auth($this->db, $this->auth, $attachment['topic_id']); + $this->phpbb_download_handle_forum_auth($attachment['topic_id']); $sql = 'SELECT forum_id, post_visibility FROM ' . POSTS_TABLE . ' @@ -142,7 +140,7 @@ class attachment extends controller $post_row = $this->db->sql_fetchrow($result); $this->db->sql_freeresult($result); - if (!$post_row || !$phpbb_content_visibility->is_visible('post', $post_row['forum_id'], $post_row)) + if (!$post_row || !$this->content_visibility->is_visible('post', $post_row['forum_id'], $post_row)) { // Attachment of a soft deleted post and the user is not allowed to see the post send_status_line(404, 'Not Found'); @@ -153,7 +151,7 @@ class attachment extends controller { // Attachment is in a private message. $post_row = array('forum_id' => false); - phpbb_download_handle_pm_auth($this->db, $this->auth, $this->user->data['user_id'], $attachment['post_msg_id']); + $this->phpbb_download_handle_pm_auth( $attachment['post_msg_id']); } $extensions = array(); @@ -180,10 +178,10 @@ class attachment extends controller { $attachment['physical_filename'] = 'thumb_' . $attachment['physical_filename']; } - else if ($display_cat == ATTACHMENT_CATEGORY_NONE && !$attachment['is_orphan'] && !phpbb_http_byte_range($attachment['filesize'])) + else if ($display_cat == ATTACHMENT_CATEGORY_NONE && !$attachment['is_orphan']) { // Update download count - phpbb_increment_downloads($this->db, $attachment['attach_id']); + $this->phpbb_increment_downloads($attachment['attach_id']); } $redirect = ''; @@ -212,7 +210,7 @@ class attachment extends controller 'thumbnail', 'redirect', ); - extract($this->phpbb_dispatcher->trigger_event('core.download_file_send_to_browser_before', compact($vars))); + extract($this->dispatcher->trigger_event('core.download_file_send_to_browser_before', compact($vars))); if (!empty($redirect)) { @@ -220,10 +218,393 @@ class attachment extends controller } else { - send_file_to_browser($attachment, $display_cat); + $this->send_file_to_browser($attachment, $display_cat); } - file_gc(); + $this->file_gc(); } } + + /** + * Send file to browser + */ + protected function send_file_to_browser($attachment, $category) + { + $filename = $attachment['physical_filename']; + + if (!$this->storage->exists($filename)) + { + send_status_line(404, 'Not Found'); + trigger_error('ERROR_NO_ATTACHMENT'); + } + + // Correct the mime type - we force application/octetstream for all files, except images + // Please do not change this, it is a security precaution + if ($category != ATTACHMENT_CATEGORY_IMAGE || strpos($attachment['mimetype'], 'image') !== 0) + { + $attachment['mimetype'] = (strpos(strtolower($this->user->browser), 'msie') !== false || strpos(strtolower($this->user->browser), 'opera') !== false) ? 'application/octetstream' : 'application/octet-stream'; + } + + if (@ob_get_length()) + { + @ob_end_clean(); + } + + // Now send the File Contents to the Browser + try + { + $file_info = $this->storage->file_info($filename); + $size = $file_info->size; + } + catch (\Exception $e) + { + $size = 0; + } + + /** + * Event to alter attachment before it is sent to browser. + * + * @event core.send_file_to_browser_before + * @var array attachment Attachment data + * @var int category Attachment category + * @var string filename Path to file, including filename + * @var int size File size + * @since 3.1.11-RC1 + */ + $vars = array( + 'attachment', + 'category', + 'filename', + 'size', + ); + extract($this->dispatcher->trigger_event('core.send_file_to_browser_before', compact($vars))); + + // To correctly display further errors we need to make sure we are using the correct headers for both (unsetting content-length may not work) + + // Check if headers already sent or not able to get the file contents. + if (headers_sent()) + { + send_status_line(500, 'Internal Server Error'); + trigger_error('UNABLE_TO_DELIVER_FILE'); + } + + // Make sure the database record for the filesize is correct + if ($size > 0 && $size != $attachment['filesize'] && strpos($attachment['physical_filename'], 'thumb_') === false) + { + // Update database record + $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' + SET filesize = ' . (int) $size . ' + WHERE attach_id = ' . (int) $attachment['attach_id']; + $this->db->sql_query($sql); + } + + // Now the tricky part... let's dance + header('Cache-Control: public'); + + // Send out the Headers. Do not set Content-Disposition to inline please, it is a security measure for users using the Internet Explorer. + header('Content-Type: ' . $attachment['mimetype']); + + header('X-Content-Type-Options: nosniff'); + + if ($category == ATTACHMENT_CATEGORY_FLASH && $this->request->variable('view', 0) === 1) + { + // We use content-disposition: inline for flash files and view=1 to let it correctly play with flash player 10 - any other disposition will fail to play inline + header('Content-Disposition: inline'); + } + else + { + header('Content-Disposition: ' . ((strpos($attachment['mimetype'], 'image') === 0) ? 'inline' : 'attachment') . '; ' . header_filename(htmlspecialchars_decode($attachment['real_filename']))); + + if (strpos($attachment['mimetype'], 'image') !== 0) + { + header('X-Download-Options: noopen'); + } + } + + if (!$this->set_modified_headers($attachment['filetime'], $this->user->browser)) + { + if ($size) + { + header("Content-Length: $size"); + } + + // Try to deliver in chunks + @set_time_limit(0); + + $fp = $this->storage->read_stream($filename); + + // Close the db connection before sending the file etc. + $this->file_gc(false); + + if ($fp !== false) + { + $output = fopen('php://output', 'w+b'); + stream_copy_to_stream($fp, $output); + fclose($fp); + } + + flush(); + } + + exit; + } + + /** + * Handles authentication when downloading attachments from a post or topic + * + * @param int $topic_id The id of the topic that we are downloading from + * + * @return null + */ + protected function phpbb_download_handle_forum_auth($topic_id) + { + $sql_array = array( + 'SELECT' => 't.topic_visibility, t.forum_id, f.forum_name, f.forum_password, f.parent_id', + 'FROM' => array( + TOPICS_TABLE => 't', + FORUMS_TABLE => 'f', + ), + 'WHERE' => 't.topic_id = ' . (int) $topic_id . ' + AND t.forum_id = f.forum_id', + ); + + $sql = $this->db->sql_build_query('SELECT', $sql_array); + $result = $this->db->sql_query($sql); + $row = $this->db->sql_fetchrow($result); + $this->db->sql_freeresult($result); + + if ($row && !$this->content_visibility->is_visible('topic', $row['forum_id'], $row)) + { + send_status_line(404, 'Not Found'); + trigger_error('ERROR_NO_ATTACHMENT'); + } + else if ($row && $this->auth->acl_get('u_download') && $this->auth->acl_get('f_download', $row['forum_id'])) + { + if ($row['forum_password']) + { + // Do something else ... ? + login_forum_box($row); + } + } + else + { + send_status_line(403, 'Forbidden'); + trigger_error('SORRY_AUTH_VIEW_ATTACH'); + } + } + + /** + * Handles authentication when downloading attachments from PMs + * + * @param int $msg_id The id of the PM that we are downloading from + * + * @return null + */ + protected function phpbb_download_handle_pm_auth($msg_id) + { + if (!$this->auth->acl_get('u_pm_download')) + { + send_status_line(403, 'Forbidden'); + trigger_error('SORRY_AUTH_VIEW_ATTACH'); + } + + $allowed = $this->phpbb_download_check_pm_auth($msg_id); + + /** + * Event to modify PM attachments download auth + * + * @event core.modify_pm_attach_download_auth + * @var bool allowed Whether the user is allowed to download from that PM or not + * @var int msg_id The id of the PM to download from + * @var int user_id The user id for auth check + * @since 3.1.11-RC1 + */ + $vars = array('allowed', 'msg_id', 'user_id'); + extract($this->dispatcher->trigger_event('core.modify_pm_attach_download_auth', compact($vars))); + + if (!$allowed) + { + send_status_line(403, 'Forbidden'); + trigger_error('ERROR_NO_ATTACHMENT'); + } + } + + /** + * Checks whether a user can download from a particular PM + * + * @param int $msg_id The id of the PM that we are downloading from + * + * @return bool Whether the user is allowed to download from that PM or not + */ + protected function phpbb_download_check_pm_auth($msg_id) + { + $user_id = $this->user->data['user_id']; + + // Check if the attachment is within the users scope... + $sql = 'SELECT msg_id + FROM ' . PRIVMSGS_TO_TABLE . ' + WHERE msg_id = ' . (int) $msg_id . ' + AND ( + user_id = ' . (int) $user_id . ' + OR author_id = ' . (int) $user_id . ' + )'; + $result = $this->db->sql_query_limit($sql, 1); + $allowed = (bool) $this->db->sql_fetchfield('msg_id'); + $this->db->sql_freeresult($result); + + return $allowed; + } + + /** + * Increments the download count of all provided attachments + * + * @param array|int $ids The attach_id of each attachment + * + * @return null + */ + protected function phpbb_increment_downloads($ids) + { + if (!is_array($ids)) + { + $ids = array($ids); + } + + $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' + SET download_count = download_count + 1 + WHERE ' . $this->db->sql_in_set('attach_id', $ids); + $this->db->sql_query($sql); + } + + /** + * Check if downloading item is allowed + */ + protected function download_allowed() + { + if (!$this->config['secure_downloads']) + { + return true; + } + + $url = htmlspecialchars_decode($this->request->header('Referer')); + + if (!$url) + { + return ($this->config['secure_allow_empty_referer']) ? true : false; + } + + // Split URL into domain and script part + $url = @parse_url($url); + + if ($url === false) + { + return ($this->config['secure_allow_empty_referer']) ? true : false; + } + + $hostname = $url['host']; + unset($url); + + $allowed = ($this->config['secure_allow_deny']) ? false : true; + $iplist = array(); + + if (($ip_ary = @gethostbynamel($hostname)) !== false) + { + foreach ($ip_ary as $ip) + { + if ($ip) + { + $iplist[] = $ip; + } + } + } + + // Check for own server... + $server_name = $this->user->host; + + // Forcing server vars is the only way to specify/override the protocol + if ($this->config['force_server_vars'] || !$server_name) + { + $server_name = $this->config['server_name']; + } + + if (preg_match('#^.*?' . preg_quote($server_name, '#') . '.*?$#i', $hostname)) + { + $allowed = true; + } + + // Get IP's and Hostnames + if (!$allowed) + { + $sql = 'SELECT site_ip, site_hostname, ip_exclude + FROM ' . SITELIST_TABLE; + $result = $this->db->sql_query($sql); + + while ($row = $this->db->sql_fetchrow($result)) + { + $site_ip = trim($row['site_ip']); + $site_hostname = trim($row['site_hostname']); + + if ($site_ip) + { + foreach ($iplist as $ip) + { + if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_ip, '#')) . '$#i', $ip)) + { + if ($row['ip_exclude']) + { + $allowed = ($this->config['secure_allow_deny']) ? false : true; + break 2; + } + else + { + $allowed = ($this->config['secure_allow_deny']) ? true : false; + } + } + } + } + + if ($site_hostname) + { + if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_hostname, '#')) . '$#i', $hostname)) + { + if ($row['ip_exclude']) + { + $allowed = ($this->config['secure_allow_deny']) ? false : true; + break; + } + else + { + $allowed = ($this->config['secure_allow_deny']) ? true : false; + } + } + } + } + $this->db->sql_freeresult($result); + } + + return $allowed; + } + + /** + * Check if the browser has the file already and set the appropriate headers- + * @returns false if a resend is in order. + */ + protected function set_modified_headers($stamp, $browser) + { + // let's see if we have to send the file at all + $last_load = $this->request->header('If-Modified-Since') ? strtotime(trim($this->request->header('If-Modified-Since'))) : false; + + if ($last_load !== false && $last_load >= $stamp) + { + send_status_line(304, 'Not Modified'); + // seems that we need those too ... browsers + header('Cache-Control: public'); + header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); + return true; + } + else + { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $stamp) . ' GMT'); + } + return false; + } } diff --git a/phpBB/phpbb/storage/controller/avatar.php b/phpBB/phpbb/storage/controller/avatar.php index 1285f110d9..f1344137e7 100644 --- a/phpBB/phpbb/storage/controller/avatar.php +++ b/phpBB/phpbb/storage/controller/avatar.php @@ -13,6 +13,7 @@ namespace phpbb\storage\controller; +use phpbb\cache\service; use phpbb\config\config; use phpbb\storage\storage; @@ -23,8 +24,9 @@ class avatar extends controller protected $allowed_extensions = ['png', 'gif', 'jpg', 'jpeg']; - public function __construct(config $config, storage $storage) + public function __construct(service $cache, config $config, storage $storage) { + $this->cache = $cache; $this->config = $config; $this->storage = $storage; } diff --git a/phpBB/phpbb/storage/controller/controller.php b/phpBB/phpbb/storage/controller/controller.php index e0da578b41..858173c069 100644 --- a/phpBB/phpbb/storage/controller/controller.php +++ b/phpBB/phpbb/storage/controller/controller.php @@ -13,37 +13,38 @@ namespace phpbb\storage\controller; + +use phpbb\cache\service; use phpbb\storage\storage; class controller { + + /** @var service */ + protected $cache; + /** @var storage */ protected $storage; - public function __construct(storage $storage) + public function __construct(service $cache, storage $storage) { + $this->cache = $cache; $this->storage = $storage; } public function handle($file) { - if (!function_exists('file_gc')) - { - global $phpbb_root_path, $phpEx; - require($phpbb_root_path . 'includes/functions_download' . '.' . $phpEx); - } - if (!$this->is_allowed($file)) { send_status_line(403, 'Forbidden'); - file_gc(); + $this->file_gc(); exit; } if (!$this->file_exists($file)) { send_status_line(404, 'Not Found'); - file_gc(); + $this->file_gc(); exit; } @@ -62,12 +63,6 @@ class controller protected function send($file) { - if (!function_exists('file_gc')) - { - global $phpbb_root_path, $phpEx; - require($phpbb_root_path . 'includes/functions_download' . '.' . $phpEx); - } - if (!headers_sent()) { header('Cache-Control: public'); @@ -95,7 +90,7 @@ class controller $fp = $this->storage->read_stream($file); // Close db connection - file_gc(false); + $this->file_gc(false); $output = fopen('php://output', 'w+b'); @@ -104,8 +99,29 @@ class controller fclose($fp); fclose($output); - // ?? flush(); } } + + /** + * Garbage Collection + * + * @param bool $exit Whether to die or not. + * + * @return null + */ + protected function file_gc($exit = true) + { + if (!empty($this->cache)) + { + $this->cache->unload(); + } + + $this->db->sql_close(); + + if ($exit) + { + exit; + } + } }