WIP fix bugs after introduction of namespaces, found trying to run the testsuite
[plcapi.git] / src / Encoder.php
1 <?php
2
3 namespace PhpXmlRpc;
4
5 use PhpXmlRpc\Helper\XMLParser;
6
7 class Encoder
8 {
9     /**
10      * Takes an xmlrpc value in PHP xmlrpcval object format and translates it into native PHP types.
11      *
12      * Works with xmlrpc requests objects as input, too.
13      *
14      * Given proper options parameter, can rebuild generic php object instances
15      * (provided those have been encoded to xmlrpc format using a corresponding
16      * option in php_xmlrpc_encode())
17      * PLEASE NOTE that rebuilding php objects involves calling their constructor function.
18      * This means that the remote communication end can decide which php code will
19      * get executed on your server, leaving the door possibly open to 'php-injection'
20      * style of attacks (provided you have some classes defined on your server that
21      * might wreak havoc if instances are built outside an appropriate context).
22      * Make sure you trust the remote server/client before eanbling this!
23      *
24      * @author Dan Libby (dan@libby.com)
25      *
26      * @param Value|Request $xmlrpc_val
27      * @param array $options if 'decode_php_objs' is set in the options array, xmlrpc structs can be decoded into php objects; if 'dates_as_objects' is set xmlrpc datetimes are decoded as php DateTime objects (standard is
28      * @return mixed
29      */
30     function decode($xmlrpc_val, $options=array())
31     {
32         switch($xmlrpc_val->kindOf())
33         {
34             case 'scalar':
35                 if (in_array('extension_api', $options))
36                 {
37                     reset($xmlrpc_val->me);
38                     list($typ,$val) = each($xmlrpc_val->me);
39                     switch ($typ)
40                     {
41                         case 'dateTime.iso8601':
42                             $xmlrpc_val->scalar = $val;
43                             $xmlrpc_val->type = 'datetime';
44                             $xmlrpc_val->timestamp = \PhpXmlRpc\Helper\Date::iso8601_decode($val);
45                             return $xmlrpc_val;
46                         case 'base64':
47                             $xmlrpc_val->scalar = $val;
48                             $xmlrpc_val->type = $typ;
49                             return $xmlrpc_val;
50                         default:
51                             return $xmlrpc_val->scalarval();
52                     }
53                 }
54                 if (in_array('dates_as_objects', $options) && $xmlrpc_val->scalartyp() == 'dateTime.iso8601')
55                 {
56                     // we return a Datetime object instead of a string
57                     // since now the constructor of xmlrpcval accepts safely strings, ints and datetimes,
58                     // we cater to all 3 cases here
59                     $out = $xmlrpc_val->scalarval();
60                     if (is_string($out))
61                     {
62                         $out = strtotime($out);
63                     }
64                     if (is_int($out))
65                     {
66                         $result = new \Datetime();
67                         $result->setTimestamp($out);
68                         return $result;
69                     }
70                     elseif (is_a($out, 'Datetime'))
71                     {
72                         return $out;
73                     }
74                 }
75                 return $xmlrpc_val->scalarval();
76             case 'array':
77                 $size = $xmlrpc_val->arraysize();
78                 $arr = array();
79                 for($i = 0; $i < $size; $i++)
80                 {
81                     $arr[] = $this->decode($xmlrpc_val->arraymem($i), $options);
82                 }
83                 return $arr;
84             case 'struct':
85                 $xmlrpc_val->structreset();
86                 // If user said so, try to rebuild php objects for specific struct vals.
87                 /// @todo should we raise a warning for class not found?
88                 // shall we check for proper subclass of xmlrpcval instead of
89                 // presence of _php_class to detect what we can do?
90                 if (in_array('decode_php_objs', $options) && $xmlrpc_val->_php_class != ''
91                     && class_exists($xmlrpc_val->_php_class))
92                 {
93                     $obj = @new $xmlrpc_val->_php_class;
94                     while(list($key,$value)=$xmlrpc_val->structeach())
95                     {
96                         $obj->$key = $this->decode($value, $options);
97                     }
98                     return $obj;
99                 }
100                 else
101                 {
102                     $arr = array();
103                     while(list($key,$value)=$xmlrpc_val->structeach())
104                     {
105                         $arr[$key] = $this->decode($value, $options);
106                     }
107                     return $arr;
108                 }
109             case 'msg':
110                 $paramcount = $xmlrpc_val->getNumParams();
111                 $arr = array();
112                 for($i = 0; $i < $paramcount; $i++)
113                 {
114                     $arr[] = $this->decode($xmlrpc_val->getParam($i));
115                 }
116                 return $arr;
117             }
118     }
119
120     /**
121      * Takes native php types and encodes them into xmlrpc PHP object format.
122      * It will not re-encode xmlrpcval objects.
123      *
124      * Feature creep -- could support more types via optional type argument
125      * (string => datetime support has been added, ??? => base64 not yet)
126      *
127      * If given a proper options parameter, php object instances will be encoded
128      * into 'special' xmlrpc values, that can later be decoded into php objects
129      * by calling php_xmlrpc_decode() with a corresponding option
130      *
131      * @author Dan Libby (dan@libby.com)
132      *
133      * @param mixed $php_val the value to be converted into an xmlrpcval object
134      * @param array $options can include 'encode_php_objs', 'auto_dates', 'null_extension' or 'extension_api'
135      * @return xmlrpcval
136      */
137     function encode($php_val, $options=array())
138     {
139         $type = gettype($php_val);
140         switch($type)
141         {
142             case 'string':
143                 if (in_array('auto_dates', $options) && preg_match('/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $php_val))
144                     $xmlrpc_val = new Value($php_val, Value::$xmlrpcDateTime);
145                 else
146                     $xmlrpc_val = new Value($php_val, Value::$xmlrpcString);
147                 break;
148             case 'integer':
149                 $xmlrpc_val = new Value($php_val, Value::$xmlrpcInt);
150                 break;
151             case 'double':
152                 $xmlrpc_val = new Value($php_val, Value::$xmlrpcDouble);
153                 break;
154                 // <G_Giunta_2001-02-29>
155                 // Add support for encoding/decoding of booleans, since they are supported in PHP
156             case 'boolean':
157                 $xmlrpc_val = new Value($php_val, Value::$xmlrpcBoolean);
158                 break;
159                 // </G_Giunta_2001-02-29>
160             case 'array':
161                 // PHP arrays can be encoded to either xmlrpc structs or arrays,
162                 // depending on wheter they are hashes or plain 0..n integer indexed
163                 // A shorter one-liner would be
164                 // $tmp = array_diff(array_keys($php_val), range(0, count($php_val)-1));
165                 // but execution time skyrockets!
166                 $j = 0;
167                 $arr = array();
168                 $ko = false;
169                 foreach($php_val as $key => $val)
170                 {
171                     $arr[$key] = $this->encode($val, $options);
172                     if(!$ko && $key !== $j)
173                     {
174                         $ko = true;
175                     }
176                     $j++;
177                 }
178                 if($ko)
179                 {
180                     $xmlrpc_val = new Value($arr, Value::$xmlrpcStruct);
181                 }
182                 else
183                 {
184                     $xmlrpc_val = new Value($arr, Value::$xmlrpcArray);
185                 }
186                 break;
187             case 'object':
188                 if(is_a($php_val, 'PhpXmlRpc\Value'))
189                 {
190                     $xmlrpc_val = $php_val;
191                 }
192                 else if(is_a($php_val, 'DateTime'))
193                 {
194                     $xmlrpc_val = new Value($php_val->format('Ymd\TH:i:s'), Value::$xmlrpcStruct);
195                 }
196                 else
197                 {
198                     $arr = array();
199                     reset($php_val);
200                     while(list($k,$v) = each($php_val))
201                     {
202                         $arr[$k] = $this->encode($v, $options);
203                     }
204                     $xmlrpc_val = new Value($arr, Value::$xmlrpcStruct);
205                     if (in_array('encode_php_objs', $options))
206                     {
207                         // let's save original class name into xmlrpcval:
208                         // might be useful later on...
209                         $xmlrpc_val->_php_class = get_class($php_val);
210                     }
211                 }
212                 break;
213             case 'NULL':
214                 if (in_array('extension_api', $options))
215                 {
216                     $xmlrpc_val = new Value('', Value::$xmlrpcString);
217                 }
218                 else if (in_array('null_extension', $options))
219                 {
220                     $xmlrpc_val = new Value('', Value::$xmlrpcNull);
221                 }
222                 else
223                 {
224                     $xmlrpc_val = new Value();
225                 }
226                 break;
227             case 'resource':
228                 if (in_array('extension_api', $options))
229                 {
230                     $xmlrpc_val = new Value((int)$php_val, Value::$xmlrpcInt);
231                 }
232                 else
233                 {
234                     $xmlrpc_val = new Value();
235                 }
236             // catch "user function", "unknown type"
237             default:
238                 // giancarlo pinerolo <ping@alt.it>
239                 // it has to return
240                 // an empty object in case, not a boolean.
241                 $xmlrpc_val = new Value();
242                 break;
243             }
244             return $xmlrpc_val;
245     }
246
247     /**
248      * Convert the xml representation of a method response, method request or single
249      * xmlrpc value into the appropriate object (a.k.a. deserialize)
250      * @param string $xml_val
251      * @param array $options
252      * @return mixed false on error, or an instance of either Value, Request or Response
253      */
254     function decode_xml($xml_val, $options=array())
255     {
256
257         /// @todo 'guestimate' encoding
258         $parser = xml_parser_create();
259         xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, true);
260         // What if internal encoding is not in one of the 3 allowed?
261         // we use the broadest one, ie. utf8!
262         if (!in_array(PhpXmlRpc::$xmlrpc_internalencoding, array('UTF-8', 'ISO-8859-1', 'US-ASCII')))
263         {
264             xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
265         }
266         else
267         {
268             xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, PhpXmlRpc::$xmlrpc_internalencoding);
269         }
270
271         $xmlRpcParser = new XMLParser();
272         xml_set_object($parser, $xmlRpcParser);
273
274         xml_set_element_handler($parser, 'xmlrpc_se_any', 'xmlrpc_ee');
275         xml_set_character_data_handler($parser, 'xmlrpc_cd');
276         xml_set_default_handler($parser, 'xmlrpc_dh');
277         if(!xml_parse($parser, $xml_val, 1))
278         {
279             $errstr = sprintf('XML error: %s at line %d, column %d',
280                         xml_error_string(xml_get_error_code($parser)),
281                         xml_get_current_line_number($parser), xml_get_current_column_number($parser));
282             error_log($errstr);
283             xml_parser_free($parser);
284             return false;
285         }
286         xml_parser_free($parser);
287         if ($xmlRpcParser->_xh['isf'] > 1) // test that $xmlrpc->_xh['value'] is an obj, too???
288         {
289             error_log($xmlRpcParser->_xh['isf_reason']);
290             return false;
291         }
292         switch ($xmlRpcParser->_xh['rt'])
293         {
294             case 'methodresponse':
295                 $v =& $xmlRpcParser->_xh['value'];
296                 if ($xmlRpcParser->_xh['isf'] == 1)
297                 {
298                     $vc = $v->structmem('faultCode');
299                     $vs = $v->structmem('faultString');
300                     $r = new Response(0, $vc->scalarval(), $vs->scalarval());
301                 }
302                 else
303                 {
304                     $r = new Response($v);
305                 }
306                 return $r;
307             case 'methodcall':
308                 $m = new Request($xmlRpcParser->_xh['method']);
309                 for($i=0; $i < count($xmlRpcParser->_xh['params']); $i++)
310                 {
311                     $m->addParam($xmlRpcParser->_xh['params'][$i]);
312                 }
313                 return $m;
314             case 'value':
315                 return $xmlRpcParser->_xh['value'];
316             default:
317                 return false;
318         }
319     }
320
321
322 /**
323  * xml charset encoding guessing helper function.
324  * Tries to determine the charset encoding of an XML chunk received over HTTP.
325  * NB: according to the spec (RFC 3023), if text/xml content-type is received over HTTP without a content-type,
326  * we SHOULD assume it is strictly US-ASCII. But we try to be more tolerant of unconforming (legacy?) clients/servers,
327  * which will be most probably using UTF-8 anyway...
328  *
329  * @param string $httpheader the http Content-type header
330  * @param string $xmlchunk xml content buffer
331  * @param string $encoding_prefs comma separated list of character encodings to be used as default (when mb extension is enabled)
332  * @return string
333  *
334  * @todo explore usage of mb_http_input(): does it detect http headers + post data? if so, use it instead of hand-detection!!!
335  */
336 function guess_encoding($httpheader='', $xmlchunk='', $encoding_prefs=null)
337 {
338     // discussion: see http://www.yale.edu/pclt/encoding/
339     // 1 - test if encoding is specified in HTTP HEADERS
340
341     //Details:
342     // LWS:           (\13\10)?( |\t)+
343     // token:         (any char but excluded stuff)+
344     // quoted string: " (any char but double quotes and cointrol chars)* "
345     // header:        Content-type = ...; charset=value(; ...)*
346     //   where value is of type token, no LWS allowed between 'charset' and value
347     // Note: we do not check for invalid chars in VALUE:
348     //   this had better be done using pure ereg as below
349     // Note 2: we might be removing whitespace/tabs that ought to be left in if
350     //   the received charset is a quoted string. But nobody uses such charset names...
351
352     /// @todo this test will pass if ANY header has charset specification, not only Content-Type. Fix it?
353     $matches = array();
354     if(preg_match('/;\s*charset\s*=([^;]+)/i', $httpheader, $matches))
355     {
356         return strtoupper(trim($matches[1], " \t\""));
357     }
358
359     // 2 - scan the first bytes of the data for a UTF-16 (or other) BOM pattern
360     //     (source: http://www.w3.org/TR/2000/REC-xml-20001006)
361     //     NOTE: actually, according to the spec, even if we find the BOM and determine
362     //     an encoding, we should check if there is an encoding specified
363     //     in the xml declaration, and verify if they match.
364     /// @todo implement check as described above?
365     /// @todo implement check for first bytes of string even without a BOM? (It sure looks harder than for cases WITH a BOM)
366     if(preg_match('/^(\x00\x00\xFE\xFF|\xFF\xFE\x00\x00|\x00\x00\xFF\xFE|\xFE\xFF\x00\x00)/', $xmlchunk))
367     {
368         return 'UCS-4';
369     }
370     elseif(preg_match('/^(\xFE\xFF|\xFF\xFE)/', $xmlchunk))
371     {
372         return 'UTF-16';
373     }
374     elseif(preg_match('/^(\xEF\xBB\xBF)/', $xmlchunk))
375     {
376         return 'UTF-8';
377     }
378
379     // 3 - test if encoding is specified in the xml declaration
380     // Details:
381     // SPACE:         (#x20 | #x9 | #xD | #xA)+ === [ \x9\xD\xA]+
382     // EQ:            SPACE?=SPACE? === [ \x9\xD\xA]*=[ \x9\xD\xA]*
383     if (preg_match('/^<\?xml\s+version\s*=\s*'. "((?:\"[a-zA-Z0-9_.:-]+\")|(?:'[a-zA-Z0-9_.:-]+'))".
384         '\s+encoding\s*=\s*' . "((?:\"[A-Za-z][A-Za-z0-9._-]*\")|(?:'[A-Za-z][A-Za-z0-9._-]*'))/",
385         $xmlchunk, $matches))
386     {
387         return strtoupper(substr($matches[2], 1, -1));
388     }
389
390     // 4 - if mbstring is available, let it do the guesswork
391     // NB: we favour finding an encoding that is compatible with what we can process
392     if(extension_loaded('mbstring'))
393     {
394         if($encoding_prefs)
395         {
396             $enc = mb_detect_encoding($xmlchunk, $encoding_prefs);
397         }
398         else
399         {
400             $enc = mb_detect_encoding($xmlchunk);
401         }
402         // NB: mb_detect likes to call it ascii, xml parser likes to call it US_ASCII...
403         // IANA also likes better US-ASCII, so go with it
404         if($enc == 'ASCII')
405         {
406             $enc = 'US-'.$enc;
407         }
408         return $enc;
409     }
410     else
411     {
412         // no encoding specified: as per HTTP1.1 assume it is iso-8859-1?
413         // Both RFC 2616 (HTTP 1.1) and 1945 (HTTP 1.0) clearly state that for text/xxx content types
414         // this should be the standard. And we should be getting text/xml as request and response.
415         // BUT we have to be backward compatible with the lib, which always used UTF-8 as default...
416         return PhpXmlRpc::$xmlrpc_defencoding;
417     }
418 }
419
420 }