3440fc6e3e4257b5a6f643f4741d5b20338ce2e7
[plcapi.git] / src / Helper / Charset.php
1 <?php
2
3 namespace PhpXmlRpc\Helper;
4
5 use PhpXmlRpc\PhpXmlRpc;
6
7 class Charset
8 {
9     // tables used for transcoding different charsets into us-ascii xml
10     protected $xml_iso88591_Entities = array("in" => array(), "out" => array());
11
12     /// @todo should we add to the latin-1 table the characters from cp_1252 range, i.e. 128 to 159 ?
13     ///       Those will NOT be present in true ISO-8859-1, but will save the unwary windows user from sending junk
14     ///       (though no luck when receiving them...)
15     ///       Note also that, apparently, while 'ISO/IEC 8859-1' has no characters defined for bytes 128 to 159,
16     ///       IANA ISO-8859-1 does have well-defined 'C1' control codes for those - wikipedia's page on latin-1 says:
17     ///       "ISO-8859-1 is the IANA preferred name for this standard when supplemented with the C0 and C1 control codes from ISO/IEC 6429."
18     ///       Check what mbstring/iconv do by default with those?
19     //
20     //protected $xml_cp1252_Entities = array('in' => array(), out' => array());
21
22     protected $charset_supersets = array(
23         'US-ASCII' => array('ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
24             'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8',
25             'ISO-8859-9', 'ISO-8859-10', 'ISO-8859-11', 'ISO-8859-12',
26             'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'UTF-8',
27             'EUC-JP', 'EUC-', 'EUC-KR', 'EUC-CN',),
28     );
29
30     /** @var Charset $instance */
31     protected static $instance = null;
32
33     /**
34      * This class is singleton for performance reasons.
35      * @todo can't we just make $xml_iso88591_Entities a static variable instead ?
36      *
37      * @return Charset
38      */
39     public static function instance()
40     {
41         if (self::$instance === null) {
42             self::$instance = new static();
43         }
44
45         return self::$instance;
46     }
47
48     /**
49      * Force usage as singleton
50      */
51     protected function __construct()
52     {
53     }
54
55     /**
56      * @param string $tableName
57      * @throws \Exception for unsupported $tableName
58      */
59     protected function buildConversionTable($tableName)
60     {
61         switch($tableName) {
62             case 'xml_iso88591_Entities':
63                 if (count($this->xml_iso88591_Entities['in'])) {
64                     return;
65                 }
66                 for ($i = 0; $i < 32; $i++) {
67                     $this->xml_iso88591_Entities["in"][] = chr($i);
68                     $this->xml_iso88591_Entities["out"][] = "&#{$i};";
69                 }
70
71                 for ($i = 160; $i < 256; $i++) {
72                     $this->xml_iso88591_Entities["in"][] = chr($i);
73                     $this->xml_iso88591_Entities["out"][] = "&#{$i};";
74                 }
75                 break;
76             /*case 'xml_cp1252_Entities':
77                 if (count($this->xml_cp1252_Entities['in'])) {
78                     return;
79                 }
80                 for ($i = 128; $i < 160; $i++)
81                 {
82                     $this->xml_cp1252_Entities['in'][] = chr($i);
83                 }
84                 $this->xml_cp1252_Entities['out'] = array(
85                     '&#x20AC;', '?',        '&#x201A;', '&#x0192;',
86                     '&#x201E;', '&#x2026;', '&#x2020;', '&#x2021;',
87                     '&#x02C6;', '&#x2030;', '&#x0160;', '&#x2039;',
88                     '&#x0152;', '?',        '&#x017D;', '?',
89                     '?',        '&#x2018;', '&#x2019;', '&#x201C;',
90                     '&#x201D;', '&#x2022;', '&#x2013;', '&#x2014;',
91                     '&#x02DC;', '&#x2122;', '&#x0161;', '&#x203A;',
92                     '&#x0153;', '?',        '&#x017E;', '&#x0178;'
93                 );
94                 $this->buildConversionTable('xml_iso88591_Entities');
95                 break;*/
96             default:
97                 throw new \Exception('Unsupported table: ' . $tableName);
98         }
99     }
100
101     /**
102      * Convert a string to the correct XML representation in a target charset.
103      *
104      * To help correct communication of non-ascii chars inside strings, regardless of the charset used when sending
105      * requests, parsing them, sending responses and parsing responses, an option is to convert all non-ascii chars
106      * present in the message into their equivalent 'charset entity'. Charset entities enumerated this way are
107      * independent of the charset encoding used to transmit them, and all XML parsers are bound to understand them.
108      * Note that in the std case we are not sending a charset encoding mime type along with http headers, so we are
109      * bound by RFC 3023 to emit strict us-ascii.
110      *
111      * @todo do a bit of basic benchmarking (strtr vs. str_replace)
112      * @todo make usage of iconv() or recode_string() or mb_string() where available
113      *
114      * @param string $data
115      * @param string $srcEncoding
116      * @param string $destEncoding
117      *
118      * @return string
119      */
120     public function encodeEntities($data, $srcEncoding = '', $destEncoding = '')
121     {
122         if ($srcEncoding == '') {
123             // lame, but we know no better...
124             $srcEncoding = PhpXmlRpc::$xmlrpc_internalencoding;
125         }
126
127         $conversion = strtoupper($srcEncoding . '_' . $destEncoding);
128         switch ($conversion) {
129             case 'ISO-8859-1_':
130             case 'ISO-8859-1_US-ASCII':
131                 $this->buildConversionTable('xml_iso88591_Entities');
132                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
133                 $escapedData = str_replace($this->xml_iso88591_Entities['in'], $this->xml_iso88591_Entities['out'], $escapedData);
134                 break;
135
136             case 'ISO-8859-1_UTF-8':
137                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
138                 $escapedData = utf8_encode($escapedData);
139                 break;
140
141             case 'ISO-8859-1_ISO-8859-1':
142             case 'US-ASCII_US-ASCII':
143             case 'US-ASCII_UTF-8':
144             case 'US-ASCII_':
145             case 'US-ASCII_ISO-8859-1':
146             case 'UTF-8_UTF-8':
147             //case 'CP1252_CP1252':
148                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
149                 break;
150
151             case 'UTF-8_':
152             case 'UTF-8_US-ASCII':
153             case 'UTF-8_ISO-8859-1':
154                 // NB: this will choke on invalid UTF-8, going most likely beyond EOF
155                 $escapedData = '';
156                 // be kind to users creating string xmlrpc values out of different php types
157                 $data = (string)$data;
158                 $ns = strlen($data);
159                 for ($nn = 0; $nn < $ns; $nn++) {
160                     $ch = $data[$nn];
161                     $ii = ord($ch);
162                     // 7 bits: 0bbbbbbb (127)
163                     if ($ii < 128) {
164                         /// @todo shall we replace this with a (supposedly) faster str_replace?
165                         switch ($ii) {
166                             case 34:
167                                 $escapedData .= '&quot;';
168                                 break;
169                             case 38:
170                                 $escapedData .= '&amp;';
171                                 break;
172                             case 39:
173                                 $escapedData .= '&apos;';
174                                 break;
175                             case 60:
176                                 $escapedData .= '&lt;';
177                                 break;
178                             case 62:
179                                 $escapedData .= '&gt;';
180                                 break;
181                             default:
182                                 $escapedData .= $ch;
183                         } // switch
184                     } // 11 bits: 110bbbbb 10bbbbbb (2047)
185                     elseif ($ii >> 5 == 6) {
186                         $b1 = ($ii & 31);
187                         $ii = ord($data[$nn + 1]);
188                         $b2 = ($ii & 63);
189                         $ii = ($b1 * 64) + $b2;
190                         $ent = sprintf('&#%d;', $ii);
191                         $escapedData .= $ent;
192                         $nn += 1;
193                     } // 16 bits: 1110bbbb 10bbbbbb 10bbbbbb
194                     elseif ($ii >> 4 == 14) {
195                         $b1 = ($ii & 15);
196                         $ii = ord($data[$nn + 1]);
197                         $b2 = ($ii & 63);
198                         $ii = ord($data[$nn + 2]);
199                         $b3 = ($ii & 63);
200                         $ii = ((($b1 * 64) + $b2) * 64) + $b3;
201                         $ent = sprintf('&#%d;', $ii);
202                         $escapedData .= $ent;
203                         $nn += 2;
204                     } // 21 bits: 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
205                     elseif ($ii >> 3 == 30) {
206                         $b1 = ($ii & 7);
207                         $ii = ord($data[$nn + 1]);
208                         $b2 = ($ii & 63);
209                         $ii = ord($data[$nn + 2]);
210                         $b3 = ($ii & 63);
211                         $ii = ord($data[$nn + 3]);
212                         $b4 = ($ii & 63);
213                         $ii = ((((($b1 * 64) + $b2) * 64) + $b3) * 64) + $b4;
214                         $ent = sprintf('&#%d;', $ii);
215                         $escapedData .= $ent;
216                         $nn += 3;
217                     }
218                 }
219
220                 // when converting to latin-1, do not be so eager with using entities for characters 160-255
221                 if ($conversion == 'UTF-8_ISO-8859-1') {
222                     $this->buildConversionTable('xml_iso88591_Entities');
223                     $escapedData = str_replace(array_slice($this->xml_iso88591_Entities['out'], 32), array_slice($this->xml_iso88591_Entities['in'], 32), $escapedData);
224                 }
225                 break;
226
227             /*
228             case 'CP1252_':
229             case 'CP1252_US-ASCII':
230                 $this->buildConversionTable('xml_cp1252_Entities');
231                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
232                 $escapedData = str_replace($this->xml_iso88591_Entities']['in'], $this->xml_iso88591_Entities['out'], $escapedData);
233                 $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
234                 break;
235             case 'CP1252_UTF-8':
236                 $this->buildConversionTable('xml_cp1252_Entities');
237                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
238                 /// @todo we could use real UTF8 chars here instead of xml entities... (note that utf_8 encode all alone will NOT convert them)
239                 $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
240                 $escapedData = utf8_encode($escapedData);
241                 break;
242             case 'CP1252_ISO-8859-1':
243                 $this->buildConversionTable('xml_cp1252_Entities');
244                 $escapedData = str_replace(array('&', '"', "'", '<', '>'), array('&amp;', '&quot;', '&apos;', '&lt;', '&gt;'), $data);
245                 // we might as well replace all funky chars with a '?' here, but we are kind and leave it to the receiving application layer to decide what to do with these weird entities...
246                 $escapedData = str_replace($this->xml_cp1252_Entities['in'], $this->xml_cp1252_Entities['out'], $escapedData);
247                 break;
248             */
249
250             default:
251                 $escapedData = '';
252                 Logger::instance()->errorLog('XML-RPC: ' . __METHOD__ . ": Converting from $srcEncoding to $destEncoding: not supported...");
253         }
254
255         return $escapedData;
256     }
257
258     /**
259      * Checks if a given charset encoding is present in a list of encodings or if it is a valid subset of any encoding
260      * in the list.
261      *
262      * @param string $encoding charset to be tested
263      * @param string|array $validList comma separated list of valid charsets (or array of charsets)
264      *
265      * @return bool
266      */
267     public function isValidCharset($encoding, $validList)
268     {
269         if (is_string($validList)) {
270             $validList = explode(',', $validList);
271         }
272         if (@in_array(strtoupper($encoding), $validList)) {
273             return true;
274         } else {
275             if (array_key_exists($encoding, $this->charset_supersets)) {
276                 foreach ($validList as $allowed) {
277                     if (in_array($allowed, $this->charset_supersets[$encoding])) {
278                         return true;
279                     }
280                 }
281             }
282
283             return false;
284         }
285     }
286
287     /**
288      * Used only for backwards compatibility
289      * @deprecated
290      *
291      * @param string $charset
292      *
293      * @return array
294      *
295      * @throws \Exception for unknown/unsupported charsets
296      */
297     public function getEntities($charset)
298     {
299         //trigger_error('Method ' . __METHOD__ . ' is deprecated', E_USER_DEPRECATED);
300
301         switch ($charset)
302         {
303             case 'iso88591':
304                 return $this->xml_iso88591_Entities;
305             default:
306                 throw new \Exception('Unsupported charset: ' . $charset);
307         }
308     }
309
310 }