complete rewrite in 6/2006 and earlier modifications * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @author RalfBecker-AT-outdoor-training.de * @copyright 2001-2016 by RalfBecker@outdoor-training.de * @package api * @version $Id$ */ namespace EGroupware\Api\Header; use EGroupware\Api; /** * Safe content type and disposition headers */ class Content { /** * Output safe content headers for user-content, mitigating risk of javascript or html * * Mitigate risk of serving javascript or css from our domain, * which will get around same origin policy and CSP! * * Mitigate risk of html downloads by using CSP or force download for IE * * @param resource|string& $content content might be changed by this call * @param string $path filename or path for content-disposition header * @param string& $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() * on return used, maybe changed, mime-type * @param int $length =0 content length, default 0 = skip that header * on return changed size * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download * @param boolean $force_download =true send content-disposition attachment header * @param boolean $no_content_type =false do not send actual content-type and content-length header, just content-disposition */ public static function safe(&$content, $path, &$mime='', &$length=0, $nocache=true, $force_download=true, $no_content_type=false) { // change old/aliased mime-types to new one, eg. image/pdf to application/pdf $mime = Api\MimeMagic::fix_mime_type($mime); // mitigate risk of serving javascript or css via webdav from our domain, // which will get around same origin policy and CSP list($type, $subtype) = explode('/', strtolower($mime)); if (!$force_download && in_array($type, array('application', 'text')) && in_array($subtype, array('javascript', 'x-javascript', 'ecmascript', 'jscript', 'vbscript', 'css'))) { // unfortunatly only Chrome and IE >= 8 allow to switch content-sniffing off with X-Content-Type-Options: nosniff if (UserAgent::type() == 'chrome' || UserAgent::type() == 'msie' && UserAgent::version() >= 8 || UserAgent::type() == 'firefox' && UserAgent::version() >= 50) { $mime = 'text/plain'; header('X-Content-Type-Options: nosniff'); // stop IE & Chrome from content-type sniffing } // for the rest we change mime-type to text/html and let code below handle it safely // this stops Safari and Firefox from using it as src attribute in a script tag // but only for "real" browsers, we dont want to modify data for our WebDAV clients elseif (isset($_SERVER['HTTP_REFERER'])) { $mime = 'text/html'; if (is_resource($content)) { $data = fread($content, $length); fclose($content); $content = $data; unset($data); } $content = '
'.$content; $length += 5; } } // mitigate risk of html (or SVG) downloads by using CSP or force download for IE if (!$force_download && in_array($mime, ['text/html', 'application/xhtml+xml', 'image/svg+xml'])) { // use CSP only for current user-agents/versions I was able to positivly test if (UserAgent::type() == 'chrome' && UserAgent::version() >= 24 || // mobile FF 24 on Android does NOT honor CSP! UserAgent::type() == 'firefox' && !UserAgent::mobile() && UserAgent::version() >= 24 || UserAgent::type() == 'safari' && !UserAgent::mobile() && UserAgent::version() >= 536 || // OS X UserAgent::type() == 'safari' && UserAgent::mobile() && UserAgent::version() >= 9537) // iOS 7 { // forbid to execute any javascript (to be precise anything but images and styles) ContentSecurityPolicy::header("image-src 'self' data: https:; style-src 'self' 'unsafe-inline' https:; default-src 'none'"); } else // everything else get's a Content-dispostion: attachment, to be on save side { //error_log(__METHOD__."('$options[path]') ".UserAgent::type().'/'.UserAgent::version().(UserAgent::mobile()?'/mobile':'').": using Content-disposition: attachment"); $force_download = true; } } // always tell browser to do no sniffing / use our content-type header('X-Content-Type-Options: nosniff'); if ($no_content_type) { if ($force_download) self::disposition(Api\Vfs::basename($path), $force_download); } else { self::type(Api\Vfs::basename($path), $mime, $length, $nocache, $force_download); } } /** * Output content-type headers for file downloads * * This function should only be used for non-user supplied content! * For uploaded files, mail attachmentes, etc, you have to use safe_content_header! * * @author Miles Lott originally in browser class * @param string $fn filename * @param string $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() * @param int $length =0 content length, default 0 = skip that header * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download * @param boolean $forceDownload =true send headers to handle as attachment/download */ public static function type($fn,$mime='',$length=0,$nocache=True,$forceDownload=true) { // if no mime-type is given or it's the default binary-type, guess it from the extension if(empty($mime) || $mime == 'application/octet-stream') { $mime = Api\MimeMagic::filename2mime($fn); } if($fn) { // Show this for all self::disposition($fn,$forceDownload); header('Content-type: '.$mime); if($length) { header('Content-length: '.$length); } if($nocache) { header('Pragma: no-cache'); header('Pragma: public'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); } } } /** * Output content-disposition header for file downloads * * @param string $fn filename * @param boolean $forceDownload =true send headers to handle as attachment/download */ public static function disposition($fn, $forceDownload=true) { if ($forceDownload) { $attachment = ' attachment;'; } else { $attachment = ' inline;'; } header('Content-disposition:'.$attachment.' filename="'.Api\Translation::to_ascii($fn).'"; filename*=utf-8\'\''.rawurlencode($fn)); } }