Browse Source

Fixing ZF-2098 and ZF-6218
- Using ftell to detect # of read bytes from stream (patch by Chris Kings-Lynne)
- Using mb_internal_encoding to make sure strlen() properly works even when overloaded by mbstring
- Adding unit tests


git-svn-id: http://framework.zend.com/svn/framework/standard/trunk@17041 44c647ce-9c0f-0410-b52a-842ac1e357ba

shahar 16 years ago
parent
commit
239a20c61a

+ 22 - 1
library/Zend/Http/Client.php

@@ -1039,9 +1039,22 @@ class Zend_Http_Client
             return '';
         }
 
+        // If mbstring overloads substr and strlen functions, we have to 
+        // override it's internal encoding
+        if (function_exists('mb_internal_encoding') && 
+           ((int) ini_get('mbstring.func_overload')) & 2) {
+
+            $mbIntEnc = mb_internal_encoding();
+            mb_internal_encoding('ASCII');
+        }
+        
         // If we have raw_post_data set, just use it as the body.
         if (isset($this->raw_post_data)) {
             $this->setHeaders(self::CONTENT_LENGTH, strlen($this->raw_post_data));
+            if (isset($mbIntEnc)) {
+                mb_internal_encoding($mbIntEnc);
+            }
+            
             return $this->raw_post_data;
         }
 
@@ -1082,6 +1095,10 @@ class Zend_Http_Client
                     break;
 
                 default:
+                    if (isset($mbIntEnc)) {
+                        mb_internal_encoding($mbIntEnc);
+                    }
+                    
                     /** @see Zend_Http_Client_Exception */
                     require_once 'Zend/Http/Client/Exception.php';
                     throw new Zend_Http_Client_Exception("Cannot handle content type '{$this->enctype}' automatically." .
@@ -1094,7 +1111,11 @@ class Zend_Http_Client
         if ($body || $this->method == self::POST || $this->method == self::PUT) {
             $this->setHeaders(self::CONTENT_LENGTH, strlen($body));
         }
-
+        
+        if (isset($mbIntEnc)) {
+            mb_internal_encoding($mbIntEnc);
+        }
+        
         return $body;
     }
 

+ 21 - 18
library/Zend/Http/Client/Adapter/Socket.php

@@ -340,22 +340,23 @@ class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interf
 
                     // Convert the hexadecimal value to plain integer
                     $chunksize = hexdec($chunksize);
-
-                    // Read chunk
-                    $left_to_read = $chunksize;
-                    while ($left_to_read > 0) {
-                        $line = @fread($this->socket, $left_to_read);
+                    
+                    // Read next chunk
+                    $read_to = ftell($this->socket) + $chunksize;
+                    
+                    do {
+                        $current_pos = ftell($this->socket);
+                        if ($current_pos >= $read_to) break;
+                        
+                        $line = @fread($this->socket, $read_to - $current_pos);
                         if ($line === false || strlen($line) === 0) {
                             $this->_checkSocketReadTimeout();
                             break;
                         } else {
                             $chunk .= $line;
-                            $left_to_read -= strlen($line);
                         }
 
-                        // Break if the connection ended prematurely
-                        if (feof($this->socket)) break;
-                    }
+                    } while (! feof($this->socket));
 
                     $chunk .= @fgets($this->socket);
                     $this->_checkSocketReadTimeout();
@@ -372,18 +373,20 @@ class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interf
         // Else, if we got the content-length header, read this number of bytes
         } elseif (isset($headers['content-length'])) {
 
-            $left_to_read = $headers['content-length'];
+            $current_pos = ftell($this->socket);
             $chunk = '';
-            while ($left_to_read > 0) {
-                $chunk = @fread($this->socket, $left_to_read);
-                $this->_checkSocketReadTimeout();
-
+            
+            for ($read_to = $current_pos + $headers['content-length'];
+                 $read_to > $current_pos;
+                 $current_pos = ftell($this->socket)) {
+                     
+                $chunk = @fread($this->socket, $read_to - $current_pos);
                 if ($chunk === false || strlen($chunk) === 0) {
+                    $this->_checkSocketReadTimeout();
                     break;
-                } else {
-                    $left_to_read -= strlen($chunk);
-                    $response .= $chunk;
-                }
+                } 
+                
+                $response .= $chunk;
 
                 // Break if the connection ended prematurely
                 if (feof($this->socket)) break;

+ 13 - 1
library/Zend/Http/Response.php

@@ -561,6 +561,15 @@ class Zend_Http_Response
     {
         $decBody = '';
 
+        // If mbstring overloads substr and strlen functions, we have to 
+        // override it's internal encoding
+        if (function_exists('mb_internal_encoding') && 
+           ((int) ini_get('mbstring.func_overload')) & 2) {
+
+            $mbIntEnc = mb_internal_encoding();
+            mb_internal_encoding('ASCII');
+        }
+        
         while (trim($body)) {
             if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
                 require_once 'Zend/Http/Exception.php';
@@ -569,10 +578,13 @@ class Zend_Http_Response
 
             $length = hexdec(trim($m[1]));
             $cut = strlen($m[0]);
-
             $decBody .= substr($body, $cut, $length);
             $body = substr($body, $cut + $length + 2);
         }
+        
+        if (isset($mbIntEnc)) {
+            mb_internal_encoding($mbIntEnc);
+        }
 
         return $decBody;
     }

+ 16 - 0
tests/Zend/Http/Client/SocketTest.php

@@ -191,6 +191,22 @@ class Zend_Http_Client_SocketTest extends Zend_Http_Client_CommonHttpTests
         $this->assertLessThan(2, $time);
     }
 
+    /**
+     * Test that a chunked response with multibyte characters is properly read
+     * 
+     * This can fail in various PHP environments - for example, when mbstring 
+     * overloads substr() and strlen(), and mbstring's internal encoding is 
+     * not a single-byte encoding. 
+     *
+     * @link http://framework.zend.com/issues/browse/ZF-6218
+     */
+    public function testMultibyteChunkedResponseZF6218()
+    {
+        $md5 = '7667818873302f9995be3798d503d8d3';
+        
+        $response = $this->client->request();
+        $this->assertEquals($md5, md5($response->getBody()));
+    }
 
     /**
      * Data Providers

+ 27 - 0
tests/Zend/Http/Client/StaticTest.php

@@ -486,6 +486,33 @@ class Zend_Http_Client_StaticTest extends PHPUnit_Framework_TestCase
     }
 
     /**
+     * Test that we properly calculate the content-length of multibyte-encoded 
+     * request body
+     * 
+     * This may file in case that mbstring overloads the substr and strlen 
+     * functions, and the mbstring internal encoding is a multibyte encoding. 
+     * 
+     * @link http://framework.zend.com/issues/browse/ZF-2098
+     */
+    public function testMultibyteRawPostDataZF2098()
+    {
+        $this->_client->setAdapter('Zend_Http_Client_Adapter_Test');
+        $this->_client->setUri('http://example.com');
+
+        $bodyFile = dirname(__FILE__) . '/_files/ZF2098-multibytepostdata.txt';
+        
+        $this->_client->setRawData(file_get_contents($bodyFile), 'text/plain');
+        $this->_client->request('POST');
+        $request = $this->_client->getLastRequest();
+        
+        if (! preg_match('/^content-length:\s+(\d+)/mi', $request, $match)) {
+            $this->fail("Unable to find content-length header in request");
+        }
+        
+        $this->assertEquals(filesize($bodyFile), (int) $match[1]);
+    }
+    
+    /**
      * Data providers
      */
 

+ 21 - 0
tests/Zend/Http/Client/_files/ZF2098-multibytepostdata.txt

@@ -0,0 +1,21 @@
+הָעֵינַיִם הָרְעֵבוֹת
+
+הָעֵינַיִם הָרְעֵבוֹת הָאֵלֶּה שֶׁכָּכָה תִּתְבַּעְנָה,
+הַשְּׂפָתַיִם הַצְּמֵאוֹת הָאֵלֶּה הַשֹּׁאֲלוֹת: נַשְּׁקֵנוּ!
+הָעֳפָרִים הָעֹרְגִים הָאֵלֶּה הַקּוֹרְאִים: תָּפְשֵׂנוּ!
+חֲמוּדוֹתַיִךְ הַצְּפוּנוֹת שֶׁשָּׂבְעָה כִשְׁאוֹל לֹא-תֵדַעְנָה;
+ 
+כָּל-עֲתֶרֶת הַגְּוִיָּה הַזֹּאת, שִׁפְעַת חֶמְדָּה מְלֵאָה,
+כָּל-הַשְּׁאֵר הַלָּזֶה, כָּל-הַבְּשָׂרִים הָאֵלֶּה שֶׁכָּכָה
+הִלְעִיטוּנִי מִמְּקוֹר תַּעֲנוּגִים, מִמַּעְיַן הַבְּרָכָה –
+לוּ יָדַעתְּ, יָפָתִי, מַה-קָּצָה בָּם נַפְשִׁי הַשְּׂבֵעָה.
+
+זַךְ הָיִיתִי, לֹא-דָלַח הַסַּעַר רִגְשׁוֹתַי הַזַּכִּים
+עדַ שֶׁבָּאת, יְפֵה-פִיָּה, וּבְרוּחֵךְ נָשַׁפְתְּ וְנִדְלַחְתִּי.
+וַאֲנִי, נַעַר פֹּתֶה, לְרַגְלַיִךְ בְּלִי-חֶמְלָה הִשְׁלַכְתִּי
+תֹּם לְבָבִי, בֹּר רוּחִי, כָּל-פִּרְחֵי נְעוּרַי הָרַכִּים.
+
+רֶגַע קָטָן מְאֻשָּׁר הָיִיתִי בְּלִי-חֹק, וָאֲבָרֵךְ
+אֶת-הַיָּד הַחֹלֶקֶת לִי מַכְאוֹב הָעֹנֶג הֶעָרֵב;
+וּבְרֶגַע קָטָן שֶׁל-תַּעֲנוּג, שֶׁל-אֹשֶׁר וָגִיל, עָלַי חָרֵב
+עוֹלָם מָלֵא – מַה-גָּדוֹל הַמְּחִיר שֶׁנָּתַתִּי בִּבְשָׂרֵךְ!

+ 16 - 0
tests/Zend/Http/Client/_files/testMultibyteChunkedResponseZF6218.php

@@ -0,0 +1,16 @@
+<?php
+
+header("Content-type: text/plain; charset=UTF-8");
+@ob_end_flush();
+@ob_implicit_flush(true);
+
+$text = <<<EOTEXT
+לִבִּי בְמִזְרָח וְאָנֹכִי בְּסוֹף מַעֲרָב
+אֵיךְ אֶטְעֲמָה אֵת אֲשֶׁר אֹכַל וְאֵיךְ יֶעֱרָב
+אֵיכָה אֲשַׁלֵּם נְדָרַי וָאֱסָרַי, בְּעוֹד
+צִיּוֹן בְּחֶבֶל אֱדוֹם וַאֲנִי בְּכֶבֶל עֲרָב
+יֵקַל בְּעֵינַי עֲזֹב כָּל טוּב סְפָרַד, כְּמוֹ
+יֵקַר בְּעֵינַי רְאוֹת עַפְרוֹת דְּבִיר נֶחֱרָב.
+EOTEXT;
+
+echo $text;

+ 15 - 0
tests/Zend/Http/ResponseTest.php

@@ -273,6 +273,21 @@ class Zend_Http_ResponseTest extends PHPUnit_Framework_TestCase
         $body = Zend_Http_Response::extractBody($this->readResponse('response_leadingws'));
         $this->assertEquals($body, "\r\n\t  \n\r\tx", 'Extracted body is not identical to expected body');
     }
+    
+    /**
+     * Test that parsing a multibyte-encoded chunked response works.
+     * 
+     * This can potentially fail on different PHP environments - for example 
+     * when mbstring.func_overload is set to overload strlen().
+     * 
+     */
+    public function testMultibyteChunkedResponse()
+    {
+        $md5 = 'ab952f1617d0e28724932401f2d3c6ae';
+
+        $response = Zend_Http_Response::fromString($this->readResponse('response_multibyte_body'));
+        $this->assertEquals($md5, md5($response->getBody()));
+    }
 
     /**
      * Helper function: read test response from file

+ 34 - 0
tests/Zend/Http/_files/response_multibyte_body

@@ -0,0 +1,34 @@
+HTTP/1.1 200 OK
+Date: Fri, 24 Jul 2009 15:22:05 GMT
+Server: Apache/2.2.11 (Unix) PHP/5.3.0 mod_ssl/2.2.11 OpenSSL/0.9.8k
+X-powered-by: PHP/5.3.0 ZendServer/4.0
+Transfer-encoding: chunked
+Content-type: text/plain; charset=utf8
+
+2ed
+אמא בואי והסירי את כוכב רב הסמל הזה מעלי
+אין לי על מי להסתלבט איתו כאן
+הנה מתאפללות עיני
+דופק לי על שער הגן
+
+טוק טוק טוק
+על דלתי מרום (אני דופק לי...)
+טוק טוק טוק
+על דלתי מרום (לא דופק...)
+knoking' on heaven's door
+like so meny times before
+
+אמא שימי ארצה את תותחי
+אין לי כח עוד ללחום
+ענן שחור ארוך נוחת עלי
+דופק לי על דלתי מרום
+
+טוק טוק טוק
+על דלתי מרום (אני דופק לי...)
+טוק טוק טוק
+על דלתי מרום (לא דופק...)
+דפוק דפוק דפוק
+על דלתי מרום
+כמו שעשית עד היום 
+0
+