Move api docs to phpdoc (wip); fix wrong property name in Response class
[plcapi.git] / src / Value.php
1 <?php
2
3 namespace PhpXmlRpc;
4
5 use PhpXmlRpc\Helper\Charset;
6
7 /**
8  * This class enables the creation and encapsulation of values for XML-RPC.
9  */
10 class Value implements \Countable, \IteratorAggregate, \ArrayAccess
11 {
12     public static $xmlrpcI4 = "i4";
13     public static $xmlrpcInt = "int";
14     public static $xmlrpcBoolean = "boolean";
15     public static $xmlrpcDouble = "double";
16     public static $xmlrpcString = "string";
17     public static $xmlrpcDateTime = "dateTime.iso8601";
18     public static $xmlrpcBase64 = "base64";
19     public static $xmlrpcArray = "array";
20     public static $xmlrpcStruct = "struct";
21     public static $xmlrpcValue = "undefined";
22     public static $xmlrpcNull = "null";
23
24     public static $xmlrpcTypes = array(
25         "i4" => 1,
26         "int" => 1,
27         "boolean" => 1,
28         "double" => 1,
29         "string" => 1,
30         "dateTime.iso8601" => 1,
31         "base64" => 1,
32         "array" => 2,
33         "struct" => 3,
34         "null" => 1,
35     );
36
37     /// @todo: do these need to be public?
38     public $me = array();
39     public $mytype = 0;
40     public $_php_class = null;
41
42     /**
43      * Build an xmlrpc value.
44      *
45      * When no value or type is passed in, the value is left uninitialized, and the value can be added later.
46      *
47      * @param mixed $val if passing in an array, all array elements should be PhpXmlRpc\Value themselves
48      * @param string $type any valid xmlrpc type name (lowercase): i4, int, boolean, string, double, dateTime.iso8601,
49      *                     base64, array, struct, null.
50      *                     If null, 'string' is assumed.
51      *                     You should refer to http://www.xmlrpc.com/spec for more information on what each of these mean.
52      */
53     public function __construct($val = -1, $type = '')
54     {
55         // optimization creep - do not call addXX, do it all inline.
56         // downside: booleans will not be coerced anymore
57         if ($val !== -1 || $type != '') {
58             switch ($type) {
59                 case '':
60                     $this->mytype = 1;
61                     $this->me['string'] = $val;
62                     break;
63                 case 'i4':
64                 case 'int':
65                 case 'double':
66                 case 'string':
67                 case 'boolean':
68                 case 'dateTime.iso8601':
69                 case 'base64':
70                 case 'null':
71                     $this->mytype = 1;
72                     $this->me[$type] = $val;
73                     break;
74                 case 'array':
75                     $this->mytype = 2;
76                     $this->me['array'] = $val;
77                     break;
78                 case 'struct':
79                     $this->mytype = 3;
80                     $this->me['struct'] = $val;
81                     break;
82                 default:
83                     error_log("XML-RPC: " . __METHOD__ . ": not a known type ($type)");
84             }
85         }
86     }
87
88     /**
89      * Add a single php value to an xmlrpc value.
90      *
91      * If the xmlrpc value is an array, the php value is added as its last element.
92      * If the xmlrpc value is empty (uninitialized), this method makes it a scalar value, and sets that value.
93      * Fails if the xmlrpc value is not an array and already initialized.
94      *
95      * @param mixed $val
96      * @param string $type allowed values: i4, int, boolean, string, double, dateTime.iso8601, base64, null.
97      *
98      * @return int 1 or 0 on failure
99      */
100     public function addScalar($val, $type = 'string')
101     {
102         $typeOf = null;
103         if (isset(static::$xmlrpcTypes[$type])) {
104             $typeOf = static::$xmlrpcTypes[$type];
105         }
106
107         if ($typeOf !== 1) {
108             error_log("XML-RPC: " . __METHOD__ . ": not a scalar type ($type)");
109             return 0;
110         }
111
112         // coerce booleans into correct values
113         // NB: we should either do it for datetimes, integers and doubles, too,
114         // or just plain remove this check, implemented on booleans only...
115         if ($type == static::$xmlrpcBoolean) {
116             if (strcasecmp($val, 'true') == 0 || $val == 1 || ($val == true && strcasecmp($val, 'false'))) {
117                 $val = true;
118             } else {
119                 $val = false;
120             }
121         }
122
123         switch ($this->mytype) {
124             case 1:
125                 error_log('XML-RPC: ' . __METHOD__ . ': scalar xmlrpc value can have only one value');
126                 return 0;
127             case 3:
128                 error_log('XML-RPC: ' . __METHOD__ . ': cannot add anonymous scalar to struct xmlrpc value');
129                 return 0;
130             case 2:
131                 // we're adding a scalar value to an array here
132                 $this->me['array'][] = new Value($val, $type);
133
134                 return 1;
135             default:
136                 // a scalar, so set the value and remember we're scalar
137                 $this->me[$type] = $val;
138                 $this->mytype = $typeOf;
139
140                 return 1;
141         }
142     }
143
144     /**
145      * Add an array of xmlrpc value objects to an xmlrpc value.
146      *
147      * If the xmlrpc value is an array, the elements are appended to the existing ones.
148      * If the xmlrpc value is empty (uninitialized), this method makes it an array value, and sets that value.
149      * Fails otherwise.
150      *
151      * @param Value[] $values
152      *
153      * @return int 1 or 0 on failure
154      *
155      * @todo add some checking for $values to be an array of xmlrpc values?
156      */
157     public function addArray($values)
158     {
159         if ($this->mytype == 0) {
160             $this->mytype = static::$xmlrpcTypes['array'];
161             $this->me['array'] = $values;
162
163             return 1;
164         } elseif ($this->mytype == 2) {
165             // we're adding to an array here
166             $this->me['array'] = array_merge($this->me['array'], $values);
167
168             return 1;
169         } else {
170             error_log('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
171             return 0;
172         }
173     }
174
175     /**
176      * Merges an array of named xmlrpc value objects into an xmlrpc value.
177      *
178      * If the xmlrpc value is a struct, the elements are merged with the existing ones (overwriting existing ones).
179      * If the xmlrpc value is empty (uninitialized), this method makes it a struct value, and sets that value.
180      * Fails otherwise.
181      *
182      * @param Value[] $values
183      *
184      * @return int 1 or 0 on failure
185      *
186      * @todo add some checking for $values to be an array?
187      */
188     public function addStruct($values)
189     {
190         if ($this->mytype == 0) {
191             $this->mytype = static::$xmlrpcTypes['struct'];
192             $this->me['struct'] = $values;
193
194             return 1;
195         } elseif ($this->mytype == 3) {
196             // we're adding to a struct here
197             $this->me['struct'] = array_merge($this->me['struct'], $values);
198
199             return 1;
200         } else {
201             error_log('XML-RPC: ' . __METHOD__ . ': already initialized as a [' . $this->kindOf() . ']');
202             return 0;
203         }
204     }
205
206     /**
207      * Returns a string containing either "struct", "array", "scalar" or "undef", describing the base type of the value.
208      *
209      * @return string
210      */
211     public function kindOf()
212     {
213         switch ($this->mytype) {
214             case 3:
215                 return 'struct';
216                 break;
217             case 2:
218                 return 'array';
219                 break;
220             case 1:
221                 return 'scalar';
222                 break;
223             default:
224                 return 'undef';
225         }
226     }
227
228     protected function serializedata($typ, $val, $charsetEncoding = '')
229     {
230         $rs = '';
231
232         if (!isset(static::$xmlrpcTypes[$typ])) {
233             return $rs;
234         }
235
236         switch (static::$xmlrpcTypes[$typ]) {
237             case 1:
238                 switch ($typ) {
239                     case static::$xmlrpcBase64:
240                         $rs .= "<${typ}>" . base64_encode($val) . "</${typ}>";
241                         break;
242                     case static::$xmlrpcBoolean:
243                         $rs .= "<${typ}>" . ($val ? '1' : '0') . "</${typ}>";
244                         break;
245                     case static::$xmlrpcString:
246                         // G. Giunta 2005/2/13: do NOT use htmlentities, since
247                         // it will produce named html entities, which are invalid xml
248                         $rs .= "<${typ}>" . Charset::instance()->encodeEntities($val, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</${typ}>";
249                         break;
250                     case static::$xmlrpcInt:
251                     case static::$xmlrpcI4:
252                         $rs .= "<${typ}>" . (int)$val . "</${typ}>";
253                         break;
254                     case static::$xmlrpcDouble:
255                         // avoid using standard conversion of float to string because it is locale-dependent,
256                         // and also because the xmlrpc spec forbids exponential notation.
257                         // sprintf('%F') could be most likely ok but it fails eg. on 2e-14.
258                         // The code below tries its best at keeping max precision while avoiding exp notation,
259                         // but there is of course no limit in the number of decimal places to be used...
260                         $rs .= "<${typ}>" . preg_replace('/\\.?0+$/', '', number_format((double)$val, 128, '.', '')) . "</${typ}>";
261                         break;
262                     case static::$xmlrpcDateTime:
263                         if (is_string($val)) {
264                             $rs .= "<${typ}>${val}</${typ}>";
265                         } elseif (is_a($val, 'DateTime')) {
266                             $rs .= "<${typ}>" . $val->format('Ymd\TH:i:s') . "</${typ}>";
267                         } elseif (is_int($val)) {
268                             $rs .= "<${typ}>" . strftime("%Y%m%dT%H:%M:%S", $val) . "</${typ}>";
269                         } else {
270                             // not really a good idea here: but what shall we output anyway? left for backward compat...
271                             $rs .= "<${typ}>${val}</${typ}>";
272                         }
273                         break;
274                     case static::$xmlrpcNull:
275                         if (PhpXmlRpc::$xmlrpc_null_apache_encoding) {
276                             $rs .= "<ex:nil/>";
277                         } else {
278                             $rs .= "<nil/>";
279                         }
280                         break;
281                     default:
282                         // no standard type value should arrive here, but provide a possibility
283                         // for xmlrpc values of unknown type...
284                         $rs .= "<${typ}>${val}</${typ}>";
285                 }
286                 break;
287             case 3:
288                 // struct
289                 if ($this->_php_class) {
290                     $rs .= '<struct php_class="' . $this->_php_class . "\">\n";
291                 } else {
292                     $rs .= "<struct>\n";
293                 }
294                 $charsetEncoder = Charset::instance();
295                 foreach ($val as $key2 => $val2) {
296                     $rs .= '<member><name>' . $charsetEncoder->encodeEntities($key2, PhpXmlRpc::$xmlrpc_internalencoding, $charsetEncoding) . "</name>\n";
297                     //$rs.=$this->serializeval($val2);
298                     $rs .= $val2->serialize($charsetEncoding);
299                     $rs .= "</member>\n";
300                 }
301                 $rs .= '</struct>';
302                 break;
303             case 2:
304                 // array
305                 $rs .= "<array>\n<data>\n";
306                 foreach ($val as $element) {
307                     //$rs.=$this->serializeval($val[$i]);
308                     $rs .= $element->serialize($charsetEncoding);
309                 }
310                 $rs .= "</data>\n</array>";
311                 break;
312             default:
313                 break;
314         }
315
316         return $rs;
317     }
318
319     /**
320      * Returns the xml representation of the value. XML prologue not included.
321      *
322      * @param string $charsetEncoding the charset to be used for serialization. if null, US-ASCII is assumed
323      *
324      * @return string
325      */
326     public function serialize($charsetEncoding = '')
327     {
328         // add check? slower, but helps to avoid recursion in serializing broken xmlrpc values...
329         //if (is_object($o) && (get_class($o) == 'xmlrpcval' || is_subclass_of($o, 'xmlrpcval')))
330         //{
331         reset($this->me);
332         list($typ, $val) = each($this->me);
333
334         return '<value>' . $this->serializedata($typ, $val, $charsetEncoding) . "</value>\n";
335         //}
336     }
337
338     /**
339      * Checks whether a struct member with a given name is present.
340      *
341      * Works only on xmlrpc values of type struct.
342      *
343      * @param string $key the name of the struct member to be looked up
344      *
345      * @return boolean
346      *
347      * @deprecated use array access, e.g. isset($val[$key])
348      */
349     public function structmemexists($key)
350     {
351         return array_key_exists($key, $this->me['struct']);
352     }
353
354     /**
355      * Returns the value of a given struct member (an xmlrpc value object in itself).
356      * Will raise a php warning if struct member of given name does not exist.
357      *
358      * @param string $key the name of the struct member to be looked up
359      *
360      * @return Value
361      *
362      * @deprecated use array access, e.g. $val[$key]
363      */
364     public function structmem($key)
365     {
366         return $this->me['struct'][$key];
367     }
368
369     /**
370      * Reset internal pointer for xmlrpc values of type struct.
371      * @deprecated iterate directly over the object using foreach instead
372      */
373     public function structreset()
374     {
375         reset($this->me['struct']);
376     }
377
378     /**
379      * Return next member element for xmlrpc values of type struct.
380      *
381      * @return Value
382      *
383      * @deprecated iterate directly over the object using foreach instead
384      */
385     public function structeach()
386     {
387         return each($this->me['struct']);
388     }
389
390     /**
391      * Returns the value of a scalar xmlrpc value (base 64 decoding is automatically handled here)
392      *
393      * @return mixed
394      */
395     public function scalarval()
396     {
397         reset($this->me);
398         list(, $b) = each($this->me);
399
400         return $b;
401     }
402
403     /**
404      * Returns the type of the xmlrpc value.
405      *
406      * For integers, 'int' is always returned in place of 'i4'.
407      *
408      * @return string
409      */
410     public function scalartyp()
411     {
412         reset($this->me);
413         list($a,) = each($this->me);
414         if ($a == static::$xmlrpcI4) {
415             $a = static::$xmlrpcInt;
416         }
417
418         return $a;
419     }
420
421     /**
422      * Returns the m-th member of an xmlrpc value of array type.
423      *
424      * @param integer $key the index of the value to be retrieved (zero based)
425      *
426      * @return Value
427      *
428      * @deprecated use array access, e.g. $val[$key]
429      */
430     public function arraymem($key)
431     {
432         return $this->me['array'][$key];
433     }
434
435     /**
436      * Returns the number of members in an xmlrpc value of array type.
437      *
438      * @return integer
439      *
440      * @deprecated use count() instead
441      */
442     public function arraysize()
443     {
444         return count($this->me['array']);
445     }
446
447     /**
448      * Returns the number of members in an xmlrpc value of struct type.
449      *
450      * @return integer
451      *
452      * @deprecated use count() instead
453      */
454     public function structsize()
455     {
456         return count($this->me['struct']);
457     }
458
459     /**
460      * Returns the number of members in an xmlrpc value:
461      * - 0 for uninitialized values
462      * - 1 for scalar values
463      * - the number of elements for struct and array values
464      *
465      * @return integer
466      */
467     public function count()
468     {
469         switch ($this->mytype) {
470             case 3:
471                 return count($this->me['struct']);
472             case 2:
473                 return count($this->me['array']);
474             case 1:
475                 return 1;
476             default:
477                 return 0;
478         }
479     }
480
481     /**
482      * Implements the IteratorAggregate interface
483      *
484      * @return ArrayIterator
485      */
486     public function getIterator() {
487         switch ($this->mytype) {
488             case 3:
489                 return new \ArrayIterator($this->me['struct']);
490             case 2:
491                 return new \ArrayIterator($this->me['array']);
492             case 1:
493                 return new \ArrayIterator($this->me);
494             default:
495                 return new \ArrayIterator();
496         }
497         return new \ArrayIterator();
498     }
499
500
501     public function offsetSet($offset, $value) {
502
503         switch ($this->mytype) {
504             case 3:
505                 if (!($value instanceof \PhpXmlRpc\Value)) {
506                     throw new \Exception('It is only possible to add Value objects to an XML-RPC Struct');
507                 }
508                 if (is_null($offset)) {
509                     // disallow struct members with empty names
510                     throw new \Exception('It is not possible to add anonymous members to an XML-RPC Struct');
511                 } else {
512                     $this->me['struct'][$offset] = $value;
513                 }
514                 return;
515             case 2:
516                 if (!($value instanceof \PhpXmlRpc\Value)) {
517                     throw new \Exception('It is only possible to add Value objects to an XML-RPC Array');
518                 }
519                 if (is_null($offset)) {
520                     $this->me['array'][] = $value;
521                 } else {
522                     // nb: we are not checking that $offset is above the existing array range...
523                     $this->me['array'][$offset] = $value;
524                 }
525                 return;
526             case 1:
527 // todo: handle i4 vs int
528                 reset($this->me);
529                 list($type,) = each($this->me);
530                 if ($type != $offset) {
531                     throw new \Exception('');
532                 }
533                 $this->me[$type] = $value;
534                 return;
535             default:
536                 // it would be nice to allow empty values to be be turned into non-empty ones this way, but we miss info to do so
537                 throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be set using array index");
538         }
539     }
540
541     public function offsetExists($offset) {
542         switch ($this->mytype) {
543             case 3:
544                 return isset($this->me['struct'][$offset]);
545             case 2:
546                 return isset($this->me['array'][$offset]);
547             case 1:
548 // todo: handle i4 vs int
549                 return $offset == $this->scalartyp();
550             default:
551                 return false;
552         }
553     }
554
555     public function offsetUnset($offset) {
556         switch ($this->mytype) {
557             case 3:
558                 unset($this->me['struct'][$offset]);
559                 return;
560             case 2:
561                 unset($this->me['array'][$offset]);
562                 return;
563             case 1:
564                 // can not remove value from a scalar
565                 throw new \Exception("XML-RPC Value is of type 'scalar' and its value can not be unset using array index");
566             default:
567                 throw new \Exception("XML-RPC Value is of type 'undef' and its value can not be unset using array index");
568         }
569     }
570
571     public function offsetGet($offset) {
572         switch ($this->mytype) {
573             case 3:
574                 return isset($this->me['struct'][$offset]) ? $this->me['struct'][$offset] : null;
575             case 2:
576                 return isset($this->me['array'][$offset]) ? $this->me['array'][$offset] : null;
577             case 1:
578 // on bad type: null or exception?
579                 reset($this->me);
580                 list($type, $value) = each($this->me);
581                 return $type == $offset ? $value : null;
582             default:
583 // return null or exception?
584                 throw new \Exception("XML-RPC Value is of type 'undef' and can not be accessed using array index");
585         }
586     }
587 }