From 4b43183df78a95e7850bac5a46718e044f362e76 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 12 Oct 2016 11:41:21 +0300 Subject: [PATCH] Fixes #10563: Fixed forming `Content-Disposition` header for file downloads (#12721) --- framework/CHANGELOG.md | 1 + framework/web/Response.php | 41 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 597750d8fc..6f2d523d7e 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -10,6 +10,7 @@ Yii Framework 2 Change Log - Bug #9277: Fixed `yii\console\controllers\AssetController` looses custom options of 'target' bundles (petrabarus, klimov-paul) - Bug #9561: Fixed `canGetProperty()` and `canSetProperty()` returns `false` for `yii\db\BaseActiveRecord` attributes (klimov-paul) - Bug #10567: Fixed `yii\console\controllers\AssetController` looses bundle override configuration, which makes it external one (klimov-paul) +- Bug #10563: Fixed forming `Content-Disposition` header for file downloads (samdark) - Bug #10681: Reverted fix of beforeValidate event calling in `yii.activeForm.js` (silverfire) - Bug #11347: Fixed `yii\widgets\Pjax::registerClientScript()` to pass custom `container` to the PJAX JS plugin (silverfire) - Bug #11352: Fixed `updateInputs()` method in `yii.activeForm.js` to prevent reading property of undefined (silverfire) diff --git a/framework/web/Response.php b/framework/web/Response.php index 3492445929..8ccbaf7d6e 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -10,6 +10,7 @@ namespace yii\web; use Yii; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; +use yii\helpers\Inflector; use yii\helpers\Url; use yii\helpers\FileHelper; use yii\helpers\StringHelper; @@ -584,7 +585,7 @@ class Response extends \yii\base\Response ->setDefault('Accept-Ranges', 'bytes') ->setDefault('Expires', '0') ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->setDefault('Content-Disposition', "$disposition; filename=\"$attachmentName\""); + ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); if ($mimeType !== null) { $headers->setDefault('Content-Type', $mimeType); @@ -708,13 +709,49 @@ class Response extends \yii\base\Response $this->getHeaders() ->setDefault($xHeader, $filePath) ->setDefault('Content-Type', $mimeType) - ->setDefault('Content-Disposition', "{$disposition}; filename=\"{$attachmentName}\""); + ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); $this->format = self::FORMAT_RAW; return $this; } + /** + * Returns Content-Disposition header value that is safe to use with both old and new browsers + * + * Fallback name: + * + * - Causes issues if contains non-ASCII characters with codes less than 32 or more than 126. + * - Causes issues if contains urlencoded characters (starting with %) or % character. Some browsers interpret + * filename="X" as urlencoded name, some aren't. + * - Causes issues if contains path separator characters such as \ or /. + * - Since value is wrapped with ", it should be escaped as \". + * - Since input could contain non-ASCII characters, fallback is obtained by transliteration. + * + * UTF name: + * + * - Causes issues if contains path separator characters such as \ or /. + * - Should be urlencoded since headers are ASCII-only. + * - Could be omitted if it exactly matches fallback name. + * + * @param string $disposition + * @param string $attachmentName + * @return string + * + * @since 2.0.10 + */ + protected function getDispositionHeaderValue($disposition, $attachmentName) + { + $fallbackName = str_replace('"', '\\"', str_replace(['%', '/', '\\'], '_', Inflector::transliterate($attachmentName, Inflector::TRANSLITERATE_LOOSE))); + $utfName = rawurlencode(str_replace(['%', '/', '\\'], '', $attachmentName)); + + $dispositionHeader = "{$disposition}; filename=\"{$fallbackName}\""; + if ($utfName !== $fallbackName) { + $dispositionHeader .= "; filename*=utf-8''{$utfName}"; + } + return $dispositionHeader; + } + /** * Redirects the browser to the specified URL. *