Skip to content

Commit

Permalink
Fix for issue #33: excessive usage of numeric charset entities when c…
Browse files Browse the repository at this point in the history
…onverting utf8 to latin-1
  • Loading branch information
gggeek committed Mar 27, 2016
1 parent 640b8c0 commit d8e180b
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 7 deletions.
5 changes: 4 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
XML-RPC for PHP version 4.0.1 - 2016/??/??
XML-RPC for PHP version 4.0.1 - 2016/3/27

* improved: all of the API documentation has been moved out of the manual and into the source code phpdoc comments

* fixed: when the internal character set is set to UTF-8 and the client sends requests (or the server responses), too
many characters were encoded as numeric entities, whereas some, like åäö, needed not not be

* fixed: the 'valtyp' property of Response was not present in all cases; the ValType property had been added by error
and has been removed

Expand Down
2 changes: 1 addition & 1 deletion src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Client

/**
* The charset encoding that will be used for serializing request sent by the client.
* If defaults to NULL, which means using US-ASCII and encoding all characters outside of the ASCII range using
* It defaults to NULL, which means using US-ASCII and encoding all characters outside of the ASCII range using
* their xml character entity representation (this has the benefit that line end characters will not be mangled in
* the transfer, a CR-LF will be preserved as well as a singe LF).
* Valid values are 'US-ASCII', 'UTF-8' and 'ISO-8859-1'
Expand Down
22 changes: 17 additions & 5 deletions src/Helper/Charset.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Charset
{
// tables used for transcoding different charsets into us-ascii xml
protected $xml_iso88591_Entities = array("in" => array(), "out" => array());
protected $xml_iso88591_utf8 = array("in" => array(), "out" => array());

/// @todo add to iso table the characters from cp_1252 range, i.e. 128 to 159?
/// These will NOT be present in true ISO-8859-1, but will save the unwary
Expand Down Expand Up @@ -93,16 +94,19 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
$srcEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
}

switch (strtoupper($srcEncoding . '_' . $destEncoding)) {
$conversion = strtoupper($srcEncoding . '_' . $destEncoding);
switch ($conversion) {
case 'ISO-8859-1_':
case 'ISO-8859-1_US-ASCII':
$escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
$escapedData = str_replace($this->xml_iso88591_Entities['in'], $this->xml_iso88591_Entities['out'], $escapedData);
break;

case 'ISO-8859-1_UTF-8':
$escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
$escapedData = utf8_encode($escapedData);
break;

case 'ISO-8859-1_ISO-8859-1':
case 'US-ASCII_US-ASCII':
case 'US-ASCII_UTF-8':
Expand All @@ -112,6 +116,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
//case 'CP1252_CP1252':
$escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
break;

case 'UTF-8_':
case 'UTF-8_US-ASCII':
case 'UTF-8_ISO-8859-1':
Expand All @@ -123,7 +128,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
for ($nn = 0; $nn < $ns; $nn++) {
$ch = $data[$nn];
$ii = ord($ch);
//1 7 0bbbbbbb (127)
// 7 bits: 0bbbbbbb (127)
if ($ii < 128) {
/// @todo shall we replace this with a (supposedly) faster str_replace?
switch ($ii) {
Expand All @@ -145,7 +150,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
default:
$escapedData .= $ch;
} // switch
} //2 11 110bbbbb 10bbbbbb (2047)
} // 11 bits: 110bbbbb 10bbbbbb (2047)
elseif ($ii >> 5 == 6) {
$b1 = ($ii & 31);
$ii = ord($data[$nn + 1]);
Expand All @@ -154,7 +159,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
$ent = sprintf('&#%d;', $ii);
$escapedData .= $ent;
$nn += 1;
} //3 16 1110bbbb 10bbbbbb 10bbbbbb
} // 16 bits: 1110bbbb 10bbbbbb 10bbbbbb
elseif ($ii >> 4 == 14) {
$b1 = ($ii & 15);
$ii = ord($data[$nn + 1]);
Expand All @@ -165,7 +170,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
$ent = sprintf('&#%d;', $ii);
$escapedData .= $ent;
$nn += 2;
} //4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
} // 21 bits: 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
elseif ($ii >> 3 == 30) {
$b1 = ($ii & 7);
$ii = ord($data[$nn + 1]);
Expand All @@ -180,7 +185,13 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
$nn += 3;
}
}

// when converting to latin-1, do not be so eager with using entities for characters 160-255
if ($conversion == 'UTF-8_ISO-8859-1') {
$escapedData = str_replace(array_slice($this->xml_iso88591_Entities['out'], 32), array_slice($this->xml_iso88591_Entities['in'], 32), $escapedData);
}
break;

/*
case 'CP1252_':
case 'CP1252_US-ASCII':
Expand All @@ -200,6 +211,7 @@ public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
$escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
break;
*/

default:
$escapedData = '';
error_log('XML-RPC: ' . __METHOD__ . ": Converting from $srcEncoding to $destEncoding: not supported...");
Expand Down
92 changes: 92 additions & 0 deletions tests/0CharsetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
/**
* @author JoakimLofgren
*/

use PhpXmlRpc\Helper\Charset;

/**
* Test conversion between encodings
*
* For Windows if you want to test the output use Consolas font
* and run the following in cmd:
* chcp 28591 (latin1)
* chcp 65001 (utf8)
*/
class CharsetTest extends PHPUnit_Framework_TestCase
{
// Consolas font should render these properly
protected $runes = "ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ";
protected $greek = "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ";
protected $russian = "Река неслася; бедный чёлн";
protected $chinese = "我能吞下玻璃而不伤身体。";
protected $latinString;

protected function setUp()
{
// construct a latin string with all chars (except control ones)
$this->latinString = "\n\r\t";
for($i = 32; $i < 127; $i++) {
$this->latinString .= chr($i);
}
for($i = 160; $i < 256; $i++) {
$this->latinString .= chr($i);
}
}

protected function utfToLatin($data)
{
return Charset::instance()->encodeEntities(
$data,
'UTF-8',
'ISO-8859-1'
);
}

public function testUtf8ToLatin1All()
{
/*$this->assertEquals(
'ISO-8859-1',
mb_detect_encoding($this->latinString, 'ISO-8859-1, UTF-8, WINDOWS-1251, ASCII', true),
'Setup latinString is not ISO-8859-1 encoded...'
);*/
$string = utf8_encode($this->latinString);
$encoded = $this->utfToLatin($string);
$this->assertEquals(str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $this->latinString), $encoded);
}

public function testUtf8ToLatin1EuroSymbol()
{
$string = 'a.b.c.å.ä.ö.€.';
$encoded = $this->utfToLatin($string);
$this->assertEquals(utf8_decode('a.b.c.å.ä.ö.&#8364;.'), $encoded);
}

public function testUtf8ToLatin1Runes()
{
$string = $this->runes;
$encoded = $this->utfToLatin($string);
$this->assertEquals('&#5792;&#5831;&#5819;&#5867;&#5842;&#5862;&#5798;&#5867;&#5792;&#5809;&#5801;&#5792;&#5794;&#5809;&#5867;&#5792;&#5825;&#5809;&#5802;&#5867;&#5815;&#5846;&#5819;&#5817;&#5862;&#5850;&#5811;&#5794;&#5847;', $encoded);
}

public function testUtf8ToLatin1Greek()
{
$string = $this->greek;
$encoded = $this->utfToLatin($string);
$this->assertEquals('&#932;&#8052; &#947;&#955;&#8182;&#963;&#963;&#945; &#956;&#959;&#8166; &#7956;&#948;&#969;&#963;&#945;&#957; &#7953;&#955;&#955;&#951;&#957;&#953;&#954;&#8052;', $encoded);
}

public function testUtf8ToLatin1Russian()
{
$string = $this->russian;
$encoded = $this->utfToLatin($string);
$this->assertEquals('&#1056;&#1077;&#1082;&#1072; &#1085;&#1077;&#1089;&#1083;&#1072;&#1089;&#1103;; &#1073;&#1077;&#1076;&#1085;&#1099;&#1081; &#1095;&#1105;&#1083;&#1085;', $encoded);
}

public function testUtf8ToLatin1Chinese()
{
$string = $this->chinese;
$encoded = $this->utfToLatin($string);
$this->assertEquals('&#25105;&#33021;&#21534;&#19979;&#29627;&#29827;&#32780;&#19981;&#20260;&#36523;&#20307;&#12290;', $encoded);
}
}

0 comments on commit d8e180b

Please sign in to comment.