Browse Source

Encode Content-Disposition header fields

Set character encoding for Content-Disposition header parameters in line with RFC 2231 for email and RFC 8187 for HTTP.
Martin Matthaei 6 years ago
parent
commit
03295d5f36
3 changed files with 95 additions and 5 deletions
  1. 36 3
      src/Http/Client/FormDataPart.php
  2. 2 2
      src/Mailer/Email.php
  3. 57 0
      tests/TestCase/Mailer/EmailTest.php

+ 36 - 3
src/Http/Client/FormDataPart.php

@@ -13,6 +13,9 @@
  */
 namespace Cake\Http\Client;
 
+use Cake\Utility\Inflector;
+use Cake\Utility\Text;
+
 /**
  * Contains the data and behavior for a single
  * part in a Multipart FormData request body.
@@ -74,17 +77,26 @@ class FormDataPart
     protected $_contentId;
 
     /**
+     * The charset attribute for the Content-Disposition header fields
+     *
+     * @var string|null
+     */
+    protected $_charset;
+
+    /**
      * Constructor
      *
      * @param string $name The name of the data.
      * @param string $value The value of the data.
      * @param string $disposition The type of disposition to use, defaults to form-data.
+     * @param string|null $charset The charset of the data.
      */
-    public function __construct($name, $value, $disposition = 'form-data')
+    public function __construct($name, $value, $disposition = 'form-data', $charset = null)
     {
         $this->_name = $name;
         $this->_value = $value;
         $this->_disposition = $disposition;
+        $this->_charset = $charset;
     }
 
     /**
@@ -198,10 +210,10 @@ class FormDataPart
         if ($this->_disposition) {
             $out .= 'Content-Disposition: ' . $this->_disposition;
             if ($this->_name) {
-                $out .= '; name="' . $this->_name . '"';
+                $out .= '; ' . $this->_headerParameterToString('name', $this->_name);
             }
             if ($this->_filename) {
-                $out .= '; filename="' . $this->_filename . '"';
+                $out .= '; ' . $this->_headerParameterToString('filename', $this->_filename);
             }
             $out .= "\r\n";
         }
@@ -219,6 +231,27 @@ class FormDataPart
 
         return $out;
     }
+
+    /**
+     * Get the string for the header parameter.
+     *
+     * If the value contains non-ASCII letters an additional header indicating
+     * the charset encoding will be set.
+     *
+     * @param string $name The name of the header parameter
+     * @param string $value The value of the header parameter
+     * @return string
+     */
+    protected function _headerParameterToString($name, $value)
+    {
+        $transliterated = Text::transliterate(str_replace('"', '', $value));
+        $return = sprintf('%s="%s"', $name, $transliterated);
+        if ($this->_charset !== null && $value !== $transliterated) {
+            $return .= sprintf("; %s*=%s''%s", $name, strtolower($this->_charset), rawurlencode($value));
+        }
+
+        return $return;
+    }
 }
 
 // @deprecated 3.4.0 Add backwards compat alias.

+ 2 - 2
src/Mailer/Email.php

@@ -2521,7 +2521,7 @@ class Email implements JsonSerializable, Serializable
                 !isset($fileInfo['contentDisposition']) ||
                 $fileInfo['contentDisposition']
             );
-            $part = new FormDataPart(false, $data, false);
+            $part = new FormDataPart('', $data, '', $this->getHeaderCharset());
 
             if ($hasDisposition) {
                 $part->disposition('attachment');
@@ -2571,7 +2571,7 @@ class Email implements JsonSerializable, Serializable
             $data = isset($fileInfo['data']) ? $fileInfo['data'] : $this->_readFile($fileInfo['file']);
 
             $msg[] = '--' . $boundary;
-            $part = new FormDataPart(false, $data, 'inline');
+            $part = new FormDataPart('', $data, 'inline', $this->getHeaderCharset());
             $part->type($fileInfo['mimetype']);
             $part->transferEncoding('base64');
             $part->contentId($fileInfo['contentId']);

+ 57 - 0
tests/TestCase/Mailer/EmailTest.php

@@ -1589,6 +1589,63 @@ class EmailTest extends TestCase
     }
 
     /**
+     * Test an attachment filename with non-ASCII characters.
+     *
+     * @return void
+     */
+    public function testSendWithNonAsciiFilenameAttachments()
+    {
+        $this->Email->setTransport('debug');
+        $this->Email->setFrom('cake@cakephp.org');
+        $this->Email->setTo('cake@cakephp.org');
+        $this->Email->setSubject('My title');
+        $this->Email->setEmailFormat('both');
+        $this->Email->setAttachments([
+            'gâteau.png' => [
+                'file' => CORE_PATH . 'VERSION.txt',
+                'contentId' => 'abc123'
+            ]
+        ]);
+        $result = $this->Email->send('Hello');
+
+        $boundary = $this->Email->getBoundary();
+        $this->assertContains('Content-Type: multipart/mixed; boundary="' . $boundary . '"', $result['headers']);
+        $expected = "--$boundary\r\n" .
+            "Content-Type: multipart/related; boundary=\"rel-$boundary\"\r\n" .
+            "\r\n" .
+            "--rel-$boundary\r\n" .
+            "Content-Type: multipart/alternative; boundary=\"alt-$boundary\"\r\n" .
+            "\r\n" .
+            "--alt-$boundary\r\n" .
+            "Content-Type: text/plain; charset=UTF-8\r\n" .
+            "Content-Transfer-Encoding: 8bit\r\n" .
+            "\r\n" .
+            'Hello' .
+            "\r\n" .
+            "\r\n" .
+            "\r\n" .
+            "--alt-$boundary\r\n" .
+            "Content-Type: text/html; charset=UTF-8\r\n" .
+            "Content-Transfer-Encoding: 8bit\r\n" .
+            "\r\n" .
+            'Hello' .
+            "\r\n" .
+            "\r\n" .
+            "\r\n" .
+            "--alt-{$boundary}--\r\n" .
+            "\r\n" .
+            "--rel-$boundary\r\n" .
+            "Content-Disposition: inline; filename=\"gateau.png\"; filename*=utf-8''g%C3%A2teau.png\r\n" .
+            "Content-Type: text/plain\r\n" .
+            "Content-Transfer-Encoding: base64\r\n" .
+            "Content-ID: <abc123>\r\n" .
+            "\r\n";
+        $this->assertContains($expected, $result['message']);
+        $this->assertContains('--rel-' . $boundary . '--', $result['message']);
+        $this->assertContains('--' . $boundary . '--', $result['message']);
+    }
+
+    /**
      * testSendWithLog method
      *
      * @return void