1<?php 2 3/** 4 +-----------------------------------------------------------------------+ 5 | This file is part of the Roundcube Webmail client | 6 | | 7 | Copyright (C) The Roundcube Dev Team | 8 | | 9 | Licensed under the GNU General Public License version 3 or | 10 | any later version with exceptions for skins & plugins. | 11 | See the README file for a full license statement. | 12 | | 13 | PURPOSE: | 14 | Delivering a specific uploaded file or mail message attachment | 15 +-----------------------------------------------------------------------+ 16 | Author: Thomas Bruederli <roundcube@gmail.com> | 17 | Author: Aleksander Machniak <alec@alec.pl> | 18 +-----------------------------------------------------------------------+ 19*/ 20 21class rcmail_action_mail_get extends rcmail_action_mail_index 22{ 23 protected static $attachment; 24 25 /** 26 * Request handler. 27 * 28 * @param array $args Arguments from the previous step(s) 29 */ 30 public function run($args = []) 31 { 32 $rcmail = rcmail::get_instance(); 33 34 // This resets X-Frame-Options for framed output (#6688) 35 $rcmail->output->page_headers(); 36 37 // show loading page 38 if (!empty($_GET['_preload'])) { 39 unset($_GET['_preload']); 40 unset($_GET['_safe']); 41 42 $url = $rcmail->url($_GET + ['_mimewarning' => 1, '_embed' => 1]); 43 $message = $rcmail->gettext('loadingdata'); 44 45 header('Content-Type: text/html; charset=' . RCUBE_CHARSET); 46 print "<html>\n<head>\n" 47 . '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n" 48 . '<meta http-equiv="content-type" content="text/html; charset=' . RCUBE_CHARSET . '">' . "\n" 49 . "</head>\n<body>\n$message\n</body>\n</html>"; 50 exit; 51 } 52 53 $attachment = new rcmail_attachment_handler; 54 $mimetype = $attachment->mimetype; 55 $filename = $attachment->filename; 56 57 self::$attachment = $attachment; 58 59 // show part page 60 if (!empty($_GET['_frame'])) { 61 $rcmail->output->set_pagetitle($filename); 62 63 // register UI objects 64 $rcmail->output->add_handlers([ 65 'messagepartframe' => [$this, 'message_part_frame'], 66 'messagepartcontrols' => [$this, 'message_part_controls'], 67 ]); 68 69 $part_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_GET); 70 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET); 71 72 // message/rfc822 preview (Note: handle also multipart/ parts, they can 73 // come from Enigma, which replaces message/rfc822 with real mimetype) 74 if ($part_id && ($mimetype == 'message/rfc822' || strpos($mimetype, 'multipart/') === 0)) { 75 $uid = preg_replace('/\.[0-9.]+/', '', $uid); 76 $uid .= '.' . $part_id; 77 78 $rcmail->output->set_env('is_message', true); 79 } 80 81 $rcmail->output->set_env('mailbox', $rcmail->storage->get_folder()); 82 $rcmail->output->set_env('uid', $uid); 83 $rcmail->output->set_env('part', $part_id); 84 $rcmail->output->set_env('filename', $filename); 85 $rcmail->output->set_env('mimetype', $mimetype); 86 87 $rcmail->output->send('messagepart'); 88 } 89 90 // render thumbnail of an image attachment 91 if (!empty($_GET['_thumb']) && $attachment->is_valid()) { 92 $thumbnail_size = $rcmail->config->get('image_thumbnail_size', 240); 93 $file_ident = $attachment->ident; 94 $thumb_name = 'thumb' . md5($file_ident . ':' . $rcmail->user->ID . ':' . $thumbnail_size); 95 $cache_file = rcube_utils::temp_filename($thumb_name, false, false); 96 97 // render thumbnail image if not done yet 98 if (!is_file($cache_file) && $attachment->body_to_file($orig_name = $cache_file . '.tmp')) { 99 $image = new rcube_image($orig_name); 100 101 if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) { 102 $mimetype = 'image/' . $imgtype; 103 } 104 else { 105 // Resize failed, we need to check the file mimetype 106 // So, we do not exit here, but goto generic file body handler below 107 $_GET['_thumb'] = 0; 108 $_REQUEST['_embed'] = 1; 109 } 110 } 111 112 if (!empty($_GET['_thumb'])) { 113 if (is_file($cache_file)) { 114 $rcmail->output->future_expire_header(3600); 115 header('Content-Type: ' . $mimetype); 116 header('Content-Length: ' . filesize($cache_file)); 117 readfile($cache_file); 118 } 119 120 exit; 121 } 122 } 123 124 // Handle attachment body (display or download) 125 if (empty($_GET['_thumb']) && $attachment->is_valid()) { 126 // require CSRF protected url for downloads 127 if (!empty($_GET['_download'])) { 128 $rcmail->request_security_check(rcube_utils::INPUT_GET); 129 } 130 131 $extensions = rcube_mime::get_mime_extensions($mimetype); 132 133 // compare file mimetype with the stated content-type headers and file extension to avoid malicious operations 134 if (!empty($_REQUEST['_embed']) && empty($_REQUEST['_nocheck'])) { 135 $file_extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); 136 137 // 1. compare filename suffix with expected suffix derived from mimetype 138 $valid = $file_extension && in_array($file_extension, (array)$extensions) 139 || empty($extensions) 140 || !empty($_REQUEST['_mimeclass']); 141 142 // 2. detect the real mimetype of the attachment part and compare it with the stated mimetype and filename extension 143 if ($valid || !$file_extension || $mimetype == 'application/octet-stream' || stripos($mimetype, 'text/') === 0) { 144 $tmp_body = $attachment->body(2048); 145 146 // detect message part mimetype 147 $real_mimetype = rcube_mime::file_content_type($tmp_body, $filename, $mimetype, true, true); 148 list($real_ctype_primary, $real_ctype_secondary) = explode('/', $real_mimetype); 149 150 // accept text/plain with any extension 151 if ($real_mimetype == 'text/plain' && self::mimetype_compare($real_mimetype, $mimetype)) { 152 $valid_extension = true; 153 } 154 // ignore differences in text/* mimetypes. Filetype detection isn't very reliable here 155 else if ($real_ctype_primary == 'text' && strpos($mimetype, $real_ctype_primary) === 0) { 156 $real_mimetype = $mimetype; 157 $valid_extension = true; 158 } 159 // ignore filename extension if mimeclass matches (#1489029) 160 else if (!empty($_REQUEST['_mimeclass']) && $real_ctype_primary == $_REQUEST['_mimeclass']) { 161 $valid_extension = true; 162 } 163 else { 164 // get valid file extensions 165 $extensions = rcube_mime::get_mime_extensions($real_mimetype); 166 $valid_extension = !$file_extension || empty($extensions) || in_array($file_extension, (array)$extensions); 167 } 168 169 if ( 170 // fix mimetype for files wrongly declared as octet-stream 171 ($mimetype == 'application/octet-stream' && $valid_extension) 172 // force detected mimetype for images (#8158) 173 || (strpos($real_mimetype, 'image/') === 0) 174 ) { 175 $mimetype = $real_mimetype; 176 } 177 178 // "fix" real mimetype the same way the original is before comparison 179 $real_mimetype = rcube_mime::fix_mimetype($real_mimetype); 180 181 $valid = $valid_extension && self::mimetype_compare($real_mimetype, $mimetype); 182 } 183 else { 184 $real_mimetype = $mimetype; 185 } 186 187 // show warning if validity checks failed 188 if (!$valid) { 189 // send blocked.gif for expected images 190 if (empty($_REQUEST['_mimewarning']) && strpos($mimetype, 'image/') === 0) { 191 // Do not cache. Failure might be the result of a misconfiguration, 192 // thus real content should be returned once fixed. 193 $content = self::get_resource_content('blocked.gif'); 194 $rcmail->output->nocacheing_headers(); 195 header("Content-Type: image/gif"); 196 header("Content-Transfer-Encoding: binary"); 197 header("Content-Length: " . strlen($content)); 198 echo $content; 199 } 200 // html warning with a button to load the file anyway 201 else { 202 $rcmail->output = new rcmail_html_page(); 203 $rcmail->output->register_inline_warning( 204 $rcmail->gettext([ 205 'name' => 'attachmentvalidationerror', 206 'vars' => [ 207 'expected' => $mimetype . (!empty($file_extension) ? rcube::Q(" (.{$file_extension})") : ''), 208 'detected' => $real_mimetype . (!empty($extensions[0]) ? " (.{$extensions[0]})" : ''), 209 ] 210 ]), 211 $rcmail->gettext('showanyway'), 212 $rcmail->url(array_merge($_GET, ['_nocheck' => 1])) 213 ); 214 215 $rcmail->output->write(); 216 } 217 218 exit; 219 } 220 } 221 222 // TIFF/WEBP to JPEG conversion, if needed 223 foreach (['tiff', 'webp'] as $type) { 224 $img_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps'][$type]); 225 if ( 226 !empty($_REQUEST['_embed']) 227 && !$img_support 228 && $attachment->image_type() == 'image/' . $type 229 && rcube_image::is_convertable('image/' . $type) 230 ) { 231 $convert2jpeg = true; 232 $mimetype = 'image/jpeg'; 233 break; 234 } 235 } 236 237 // deliver part content 238 if ($mimetype == 'text/html' && empty($_GET['_download'])) { 239 $rcmail->output = new rcmail_html_page(); 240 $out = ''; 241 242 // Check if we have enough memory to handle the message in it 243 // #1487424: we need up to 10x more memory than the body 244 if (!rcube_utils::mem_check($attachment->size * 10)) { 245 $rcmail->output->register_inline_warning( 246 $rcmail->gettext('messagetoobig'), 247 $rcmail->gettext('download'), 248 $rcmail->url(array_merge($_GET, ['_download' => 1])) 249 ); 250 } 251 else { 252 // render HTML body 253 $out = $attachment->html(); 254 255 // insert remote objects warning into HTML body 256 if (self::$REMOTE_OBJECTS) { 257 $rcmail->output->register_inline_warning( 258 $rcmail->gettext('blockedresources'), 259 $rcmail->gettext('allow'), 260 $rcmail->url(array_merge($_GET, ['_safe' => 1])) 261 ); 262 } 263 } 264 265 $rcmail->output->write($out); 266 exit; 267 } 268 269 // add filename extension if missing 270 if (!pathinfo($filename, PATHINFO_EXTENSION) && ($extensions = rcube_mime::get_mime_extensions($mimetype))) { 271 $filename .= '.' . $extensions[0]; 272 } 273 274 $rcmail->output->download_headers($filename, [ 275 'type' => $mimetype, 276 'type_charset' => $attachment->charset, 277 'disposition' => !empty($_GET['_download']) ? 'attachment' : 'inline', 278 ]); 279 280 // handle tiff to jpeg conversion 281 if (!empty($convert2jpeg)) { 282 $file_path = rcube_utils::temp_filename('attmnt'); 283 284 // convert image to jpeg and send it to the browser 285 if ($attachment->body_to_file($file_path)) { 286 $image = new rcube_image($file_path); 287 if ($image->convert(rcube_image::TYPE_JPG, $file_path)) { 288 header("Content-Length: " . filesize($file_path)); 289 readfile($file_path); 290 } 291 } 292 } 293 else { 294 $attachment->output($mimetype); 295 } 296 297 exit; 298 } 299 300 // if we arrive here, the requested part was not found 301 header('HTTP/1.1 404 Not Found'); 302 exit; 303 } 304 305 /** 306 * Compares two mimetype strings with making sure that 307 * e.g. image/bmp and image/x-ms-bmp are treated as equal. 308 */ 309 public static function mimetype_compare($type1, $type2) 310 { 311 $regexp = '~/(x-ms-|x-)~'; 312 $type1 = preg_replace($regexp, '/', $type1); 313 $type2 = preg_replace($regexp, '/', $type2); 314 315 return $type1 === $type2; 316 } 317 318 /** 319 * Attachment properties table 320 */ 321 public static function message_part_controls($attrib) 322 { 323 if (!self::$attachment->is_valid()) { 324 return ''; 325 } 326 327 $rcmail = rcmail::get_instance(); 328 $table = new html_table(['cols' => 2]); 329 330 $table->add('title', rcube::Q($rcmail->gettext('namex')).':'); 331 $table->add('header', rcube::Q(self::$attachment->filename)); 332 333 $table->add('title', rcube::Q($rcmail->gettext('type')).':'); 334 $table->add('header', rcube::Q(self::$attachment->mimetype)); 335 336 $table->add('title', rcube::Q($rcmail->gettext('size')).':'); 337 $table->add('header', rcube::Q(self::$attachment->size())); 338 339 return $table->show($attrib); 340 } 341 342 /** 343 * Attachment preview frame 344 */ 345 public static function message_part_frame($attrib) 346 { 347 $rcmail = rcmail::get_instance(); 348 349 if ($rcmail->output->get_env('is_message')) { 350 $url = [ 351 'task' => 'mail', 352 'action' => 'preview', 353 'uid' => $rcmail->output->get_env('uid'), 354 'mbox' => $rcmail->output->get_env('mailbox'), 355 ]; 356 } 357 else { 358 $mimetype = $rcmail->output->get_env('mimetype'); 359 $url = $_GET; 360 $url[strpos($mimetype, 'text/') === 0 ? '_embed' : '_preload'] = 1; 361 unset($url['_frame']); 362 } 363 364 $url['_framed'] = 1; // For proper X-Frame-Options:deny handling 365 366 $attrib['src'] = $rcmail->url($url); 367 368 $rcmail->output->add_gui_object('messagepartframe', $attrib['id']); 369 370 return html::iframe($attrib); 371 } 372} 373