XRL  latest
Simple XML-RPC Library (both client and server)
Decoder.php
1 <?php
2 /*
3  * This file is part of XRL, a simple XML-RPC Library for PHP.
4  *
5  * Copyright (c) 2012, XRL Team. All rights reserved.
6  * XRL is licensed under the 3-clause BSD License.
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11 
12 namespace fpoirotte\XRL;
13 
22 {
24  protected $validate;
25 
27  protected $currentNode;
28 
30  protected $timezone;
31 
33  protected static $types = array(
34  \XMLReader::NONE => 'NONE',
35  \XMLReader::ELEMENT => 'ELEMENT',
36  \XMLReader::ATTRIBUTE => 'ATTRIBUTE',
37  \XMLReader::TEXT => 'TEXT',
38  \XMLReader::CDATA => 'CDATA',
39  \XMLReader::ENTITY_REF => 'ENTITY_REF',
40  \XMLReader::ENTITY => 'ENTITY',
41  \XMLReader::PI => 'PI',
42  \XMLReader::COMMENT => 'COMMENT',
43  \XMLReader::DOC => 'DOC',
44  \XMLReader::DOC_TYPE => 'DOC_TYPE',
45  \XMLReader::DOC_FRAGMENT => 'DOC_FRAGMENT',
46  \XMLReader::NOTATION => 'NOTATION',
47  \XMLReader::WHITESPACE => 'WHITESPACE',
48  \XMLReader::SIGNIFICANT_WHITESPACE => 'SIGNIFICANT_WHITESPACE',
49  \XMLReader::END_ELEMENT => 'END_ELEMENT',
50  \XMLReader::END_ENTITY => 'END_ENTITY',
51  \XMLReader::XML_DECLARATION => 'XML_DECLARATION',
52  );
53 
71  public function __construct(
72  \DateTimeZone $timezone = null,
73  $validate = true
74  ) {
75  if (!is_bool($validate)) {
76  throw new \InvalidArgumentException('Not a boolean');
77  }
78 
79  if ($timezone === null) {
80  try {
81  $timezone = new \DateTimeZone(@date_default_timezone_get());
82  } catch (\Exception $e) {
83  throw new \InvalidArgumentException($e->getMessage(), $e->getCode());
84  }
85  }
86 
87  $this->validate = $validate;
88  $this->currentNode = null;
89  $this->timezone = $timezone;
90  }
91 
114  protected function getReader($URI, $request)
115  {
116  if (!is_string($URI)) {
117  throw new \InvalidArgumentException('Not a string');
118  }
119 
120  if (!is_bool($request)) {
121  throw new \InvalidArgumentException('Not a boolean');
122  }
123 
124  $this->currentNode = null;
125  $reader = new \XMLReader();
126  $reader->open($URI, null, LIBXML_NONET | LIBXML_NOENT);
127  if ($this->validate) {
128  $schema = dirname(__DIR__) .
129  DIRECTORY_SEPARATOR . 'data' .
130  DIRECTORY_SEPARATOR;
131  $schema .= $request ? 'request.rng' : 'response.rng';
132  $reader->setRelaxNGSchema($schema);
133  }
134  return $reader;
135  }
136 
147  protected function readNode($reader)
148  {
149  if ($this->currentNode !== null) {
150  return $this->currentNode;
151  }
152 
153  $this->currentNode = new \fpoirotte\XRL\Node($reader, $this->validate, true);
154  return $this->currentNode;
155  }
156 
162  protected function prepareNextNode()
163  {
164  if (!$this->currentNode->emptyNodeExpansionWorked()) {
165  $this->currentNode = null;
166  }
167  }
168 
188  protected function expectStartTag($reader, $expectedTag)
189  {
190  $node = $this->readNode($reader);
191 
192  $type = $node->nodeType;
193  if ($type !== \XMLReader::ELEMENT) {
194  $type = isset(self::$types[$type]) ? self::$types[$type] : "#$type";
195  throw new \InvalidArgumentException(
196  "Expected an opening $expectedTag tag ".
197  "but got a node of type $type instead"
198  );
199  }
200 
201  $readTag = $node->name;
202  if ($readTag !== $expectedTag) {
203  throw new \InvalidArgumentException(
204  "Got opening tag for $readTag instead of $expectedTag"
205  );
206  }
207 
208  $this->prepareNextNode();
209  }
210 
230  protected function expectEndTag($reader, $expectedTag)
231  {
232  $node = $this->readNode($reader);
233 
234  $type = $node->nodeType;
235  if ($type !== \XMLReader::END_ELEMENT) {
236  $type = isset(self::$types[$type]) ? self::$types[$type] : "#$type";
237  throw new \InvalidArgumentException(
238  "Expected a closing $expectedTag tag ".
239  "but got a node of type $type instead"
240  );
241  }
242 
243  $readTag = $node->name;
244  if ($readTag !== $expectedTag) {
245  throw new \InvalidArgumentException(
246  "Got closing tag for $readTag instead of $expectedTag"
247  );
248  }
249 
250  $this->prepareNextNode();
251  }
252 
270  protected function parseText($reader)
271  {
272  $node = $this->readNode($reader);
273 
274  $type = $node->nodeType;
275  if ($type !== \XMLReader::TEXT) {
276  $type = isset(self::$types[$type]) ? self::$types[$type] : "#$type";
277  throw new \InvalidArgumentException(
278  "Expected a text node, but got ".
279  "a node of type $type instead"
280  );
281  }
282 
283  $value = $node->value;
284  $this->prepareNextNode();
285  return $value;
286  }
287 
310  protected static function checkType(array $allowedTypes, $type, $value)
311  {
312  if (count($allowedTypes) && !in_array($type, $allowedTypes)) {
313  $allowed = implode(', ', $allowedTypes);
314  throw new \InvalidArgumentException(
315  "Expected one of: $allowed; got $type"
316  );
317  }
318 
319  return $value;
320  }
321 
342  protected function decodeValue(\XMLReader $reader, array $allowedTypes = array())
343  {
344  // Basic types.
345  $types = array(
346  // Support for the <nil> extension
347  // (http://ontosys.com/xml-rpc/extensions.php)
348  'nil' => '\\fpoirotte\\XRL\\Types\\Nil',
349  'i4' => '\\fpoirotte\\XRL\\Types\\I4',
350  'i8' => '\\fpoirotte\\XRL\\Types\\I8',
351  'int' => '\\fpoirotte\\XRL\\Types\\IntType',
352  'boolean' => '\\fpoirotte\\XRL\\Types\\Boolean',
353  'string' => '\\fpoirotte\\XRL\\Types\\StringType',
354  'double' => '\\fpoirotte\\XRL\\Types\\Double',
355  'dateTime.iso8601' => '\\fpoirotte\\XRL\\Types\\DateTimeIso8601',
356  'base64' => '\\fpoirotte\\XRL\\Types\\Base64',
357 
358  // Some Apache extensions.
359  // See http://ws.apache.org/xmlrpc/types.html
360  '{http://ws.apache.org/xmlrpc/namespaces/extensions}nil'
361  => '\\fpoirotte\\XRL\\Types\\Nil',
362  '{http://ws.apache.org/xmlrpc/namespaces/extensions}i1'
363  => '\\fpoirotte\\XRL\\Types\\I1',
364  '{http://ws.apache.org/xmlrpc/namespaces/extensions}i8'
365  => '\\fpoirotte\\XRL\\Types\\I8',
366  '{http://ws.apache.org/xmlrpc/namespaces/extensions}i2'
367  => '\\fpoirotte\\XRL\\Types\\I2',
368  '{http://ws.apache.org/xmlrpc/namespaces/extensions}biginteger'
369  => '\\fpoirotte\\XRL\\Types\\BigInteger',
370  '{http://ws.apache.org/xmlrpc/namespaces/extensions}dateTime'
371  => '\\fpoirotte\\XRL\\Types\\DateTime',
372  );
373 
374  foreach ($types as $type => $cls) {
375  try {
376  $this->expectStartTag($reader, $type);
377  } catch (\InvalidArgumentException $e) {
378  continue;
379  }
380 
381  try {
382  $value = $this->parseText($reader);
383  } catch (\InvalidArgumentException $e) {
384  // Both "string" & "base64" may refer
385  // to an empty string.
386  if ($type !== 'string' && $type !== 'base64') {
387  throw $e;
388  }
389  $value = '';
390  }
391  $this->expectEndTag($reader, $type);
392  $value = $cls::read($value, $this->timezone);
393  return self::checkType($allowedTypes, $type, $value);
394  }
395 
396  // Handle structures.
397  $error = null;
398  try {
399  $this->expectStartTag($reader, 'struct');
400  } catch (\InvalidArgumentException $error) {
401  }
402 
403  if (!$error) {
404  $value = new \fpoirotte\XRL\Types\Struct(array());
405  // Read values.
406  while (true) {
407  $error = null;
408  try {
409  $this->expectStartTag($reader, 'member');
410  } catch (\InvalidArgumentException $error) {
411  }
412 
413  if ($error) {
414  break;
415  }
416 
417  // Read key.
418  $this->expectStartTag($reader, 'name');
419  $key = $this->parseText($reader);
420  $this->expectEndTag($reader, 'name');
421 
422  $this->expectStartTag($reader, 'value');
423  $value[$key] = $this->decodeValue($reader);
424  $this->expectEndTag($reader, 'value');
425  $this->expectEndTag($reader, 'member');
426  }
427  $this->expectEndTag($reader, 'struct');
428  return self::checkType($allowedTypes, 'struct', $value);
429  }
430 
431  // Handle arrays.
432  $error = null;
433  try {
434  $this->expectStartTag($reader, 'array');
435  } catch (\InvalidArgumentException $error) {
436  }
437 
438  if (!$error) {
439  $value = new \fpoirotte\XRL\Types\ArrayType(array());
440  $this->expectStartTag($reader, 'data');
441  // Read values.
442  while (true) {
443  $error = null;
444  try {
445  $this->expectStartTag($reader, 'value');
446  } catch (\InvalidArgumentException $error) {
447  }
448 
449  if ($error) {
450  break;
451  }
452 
453  $value[] = $this->decodeValue($reader);
454  $this->expectEndTag($reader, 'value');
455  }
456  $this->expectEndTag($reader, 'data');
457  $this->expectEndTag($reader, 'array');
458  return self::checkType($allowedTypes, 'array', $value);
459  }
460 
461  // Handle Apache's <dom> type.
462  $error = null;
463  try {
464  $this->expectStartTag($reader, '{http://ws.apache.org/xmlrpc/namespaces/extensions}dom');
465  } catch (\InvalidArgumentException $error) {
466  }
467 
468  if (!$error) {
469  $value = \fpoirotte\XRL\Types\Dom::read($reader->readInnerXML());
470  // Move to next sibling, skipping subtrees, and save the result.
471  $this->currentNode = new \fpoirotte\XRL\Node($reader, $this->validate, false);
472  return self::checkType(
473  $allowedTypes,
474  '{http://ws.apache.org/xmlrpc/namespaces/extensions}dom',
475  $value
476  );
477  }
478 
479  // Default type (string).
480  try {
481  $value = $this->parseText($reader);
482  } catch (\InvalidArgumentException $e) {
483  $value = '';
484  }
485  $value = new \fpoirotte\XRL\Types\StringType($value);
486  return self::checkType($allowedTypes, 'string', $value);
487  }
488 
490  public function decodeRequest($URI)
491  {
492  if (!is_string($URI)) {
493  throw new \InvalidArgumentException('A string was expected');
494  }
495 
496  $reader = $this->getReader($URI, true);
497  $ldel = libxml_disable_entity_loader(true);
498  $luie = libxml_use_internal_errors(true);
499  libxml_clear_errors();
500  try {
501  $this->expectStartTag($reader, 'methodCall');
502  $this->expectStartTag($reader, 'methodName');
503  $methodName = $this->parseText($reader);
504  $this->expectEndTag($reader, 'methodName');
505 
506  $params = array();
507  $emptyParams = null;
508  try {
509  $this->expectStartTag($reader, 'params');
510  } catch (\InvalidArgumentException $emptyParams) {
511  // Nothing to do here (no arguments given).
512  }
513 
514  if (!$emptyParams) {
515  $endOfParams = null;
516  while (true) {
517  try {
518  $this->expectStartTag($reader, 'param');
519  } catch (\InvalidArgumentException $endOfParams) {
520  // Nothing to do here (end of arguments).
521  }
522 
523  if ($endOfParams) {
524  break;
525  }
526 
527  $this->expectStartTag($reader, 'value');
528  $params[] = $this->decodeValue($reader);
529  $this->expectEndTag($reader, 'value');
530  $this->expectEndTag($reader, 'param');
531  }
532  $this->expectEndTag($reader, 'params');
533  }
534  $this->expectEndTag($reader, 'methodCall');
535 
536  $endOfFile = null;
537  try {
538  $this->readNode($reader);
539  } catch (\InvalidArgumentException $endOfFile) {
540  }
541 
542  if (!$endOfFile) {
543  throw new \InvalidArgumentException('Expected end of document');
544  }
545 
546  $request = new \fpoirotte\XRL\Request($methodName, $params);
547  libxml_disable_entity_loader($ldel);
548  libxml_clear_errors();
549  libxml_use_internal_errors($luie);
550  return $request;
551  } catch (\Exception $e) {
552  libxml_disable_entity_loader($ldel);
553  libxml_clear_errors();
554  libxml_use_internal_errors($luie);
555  throw $e;
556  }
557  }
558 
560  public function decodeResponse($URI)
561  {
562  if (!is_string($URI)) {
563  throw new \InvalidArgumentException('A string was expected');
564  }
565 
566  $error = null;
567  $reader = $this->getReader($URI, false);
568  $ldel = libxml_disable_entity_loader(true);
569  $luie = libxml_use_internal_errors(true);
570  libxml_clear_errors();
571  try {
572  $this->expectStartTag($reader, 'methodResponse');
573  try {
574  // Try to parse a successful response first.
575  $this->expectStartTag($reader, 'params');
576  $this->expectStartTag($reader, 'param');
577  $this->expectStartTag($reader, 'value');
578  $response = $this->decodeValue($reader);
579  $this->expectEndTag($reader, 'value');
580  $this->expectEndTag($reader, 'param');
581  $this->expectEndTag($reader, 'params');
582  } catch (\InvalidArgumentException $error) {
583  // Try to parse a fault instead.
584  $this->expectStartTag($reader, 'fault');
585  $this->expectStartTag($reader, 'value');
586 
587  $response = $this->decodeValue($reader);
588  if (!($response instanceof \fpoirotte\XRL\Types\Struct) ||
589  count($response) != 2) {
590  throw new \UnexpectedValueException(
591  'An associative array with exactly '.
592  'two entries was expected'
593  );
594  }
595 
596  if (!isset($response['faultCode'])) {
597  throw new \DomainException('The failure lacks a faultCode');
598  }
599 
600  if (!isset($response['faultString'])) {
601  throw new \DomainException('The failure lacks a faultString');
602  }
603 
604  $this->expectEndTag($reader, 'value');
605  $this->expectEndTag($reader, 'fault');
606  }
607  $this->expectEndTag($reader, 'methodResponse');
608 
609  $endOfFile = null;
610  try {
611  $this->readNode($reader);
612  } catch (\InvalidArgumentException $endOfFile) {
613  }
614 
615  if (!$endOfFile) {
616  throw new \InvalidArgumentException('Expected end of document');
617  }
618 
619  if ($error) {
620  throw new \fpoirotte\XRL\Exception(
621  (string) $response['faultString'],
622  $response['faultCode']->get()
623  );
624  }
625 
626  libxml_disable_entity_loader($ldel);
627  libxml_clear_errors();
628  libxml_use_internal_errors($luie);
629  return $response;
630  } catch (\Exception $e) {
631  libxml_disable_entity_loader($ldel);
632  libxml_clear_errors();
633  libxml_use_internal_errors($luie);
634  throw $e;
635  }
636  }
637 }
Interface for an XML-RPC decoder.
$validate
Whether the documents should be validated or not.
Definition: Decoder.php:24
$timezone
Timezone used to decode date/times.
Definition: Decoder.php:30
static read($value,\DateTimeZone $timezone=null)
static $types
Names for the various types of XML nodes in libxml.
Definition: Decoder.php:33
An exception that is used to represent XML-RPC errors.
Definition: Exception.php:21
expectEndTag($reader, $expectedTag)
Definition: Decoder.php:230
static checkType(array $allowedTypes, $type, $value)
Definition: Decoder.php:310
decodeValue(\XMLReader $reader, array $allowedTypes=array())
Definition: Decoder.php:342
getReader($URI, $request)
Definition: Decoder.php:114
$currentNode
The fpoirotte::XRL::Node currently being processed.
Definition: Decoder.php:27
A decoder that can process XML-RPC requests and responses, with optional XML validation.
Definition: Decoder.php:21
expectStartTag($reader, $expectedTag)
Definition: Decoder.php:188
__construct(\DateTimeZone $timezone=null, $validate=true)
Definition: Decoder.php:71