XmlLocation.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. <?php
  2. namespace GuzzleHttp\Command\Guzzle\RequestLocation;
  3. use GuzzleHttp\Command\CommandInterface;
  4. use GuzzleHttp\Command\Guzzle\Operation;
  5. use GuzzleHttp\Command\Guzzle\Parameter;
  6. use GuzzleHttp\Psr7;
  7. use Psr\Http\Message\RequestInterface;
  8. /**
  9. * Creates an XML document
  10. */
  11. class XmlLocation extends AbstractLocation
  12. {
  13. /** @var \XMLWriter XML writer resource */
  14. private $writer;
  15. /** @var string Content-Type header added when XML is found */
  16. private $contentType;
  17. /** @var Parameter[] Buffered elements to write */
  18. private $buffered = [];
  19. /**
  20. * @param string $locationName Name of the location
  21. * @param string $contentType Set to a non-empty string to add a
  22. * Content-Type header to a request if any XML content is added to the
  23. * body. Pass an empty string to disable the addition of the header.
  24. */
  25. public function __construct($locationName = 'xml', $contentType = 'application/xml')
  26. {
  27. parent::__construct($locationName);
  28. $this->contentType = $contentType;
  29. }
  30. /**
  31. * @param CommandInterface $command
  32. * @param RequestInterface $request
  33. * @param Parameter $param
  34. *
  35. * @return RequestInterface
  36. */
  37. public function visit(
  38. CommandInterface $command,
  39. RequestInterface $request,
  40. Parameter $param
  41. ) {
  42. // Buffer and order the parameters to visit based on if they are
  43. // top-level attributes or child nodes.
  44. // @link https://github.com/guzzle/guzzle/pull/494
  45. if ($param->getData('xmlAttribute')) {
  46. array_unshift($this->buffered, $param);
  47. } else {
  48. $this->buffered[] = $param;
  49. }
  50. return $request;
  51. }
  52. /**
  53. * @param CommandInterface $command
  54. * @param RequestInterface $request
  55. * @param Operation $operation
  56. *
  57. * @return RequestInterface
  58. */
  59. public function after(
  60. CommandInterface $command,
  61. RequestInterface $request,
  62. Operation $operation
  63. ) {
  64. foreach ($this->buffered as $param) {
  65. $this->visitWithValue(
  66. $command[$param->getName()],
  67. $param,
  68. $operation
  69. );
  70. }
  71. $this->buffered = [];
  72. $additional = $operation->getAdditionalParameters();
  73. if ($additional && $additional->getLocation() == $this->locationName) {
  74. foreach ($command->toArray() as $key => $value) {
  75. if (!$operation->hasParam($key)) {
  76. $additional->setName($key);
  77. $this->visitWithValue($value, $additional, $operation);
  78. }
  79. }
  80. $additional->setName(null);
  81. }
  82. // If data was found that needs to be serialized, then do so
  83. $xml = '';
  84. if ($this->writer) {
  85. $xml = $this->finishDocument($this->writer);
  86. } elseif ($operation->getData('xmlAllowEmpty')) {
  87. // Check if XML should always be sent for the command
  88. $writer = $this->createRootElement($operation);
  89. $xml = $this->finishDocument($writer);
  90. }
  91. if ($xml !== '') {
  92. $request = $request->withBody(Psr7\stream_for($xml));
  93. // Don't overwrite the Content-Type if one is set
  94. if ($this->contentType && !$request->hasHeader('Content-Type')) {
  95. $request = $request->withHeader('Content-Type', $this->contentType);
  96. }
  97. }
  98. $this->writer = null;
  99. return $request;
  100. }
  101. /**
  102. * Create the root XML element to use with a request
  103. *
  104. * @param Operation $operation Operation object
  105. *
  106. * @return \XMLWriter
  107. */
  108. protected function createRootElement(Operation $operation)
  109. {
  110. static $defaultRoot = ['name' => 'Request'];
  111. // If no root element was specified, then just wrap the XML in 'Request'
  112. $root = $operation->getData('xmlRoot') ?: $defaultRoot;
  113. // Allow the XML declaration to be customized with xmlEncoding
  114. $encoding = $operation->getData('xmlEncoding');
  115. $writer = $this->startDocument($encoding);
  116. $writer->startElement($root['name']);
  117. // Create the wrapping element with no namespaces if no namespaces were present
  118. if (!empty($root['namespaces'])) {
  119. // Create the wrapping element with an array of one or more namespaces
  120. foreach ((array) $root['namespaces'] as $prefix => $uri) {
  121. $nsLabel = 'xmlns';
  122. if (!is_numeric($prefix)) {
  123. $nsLabel .= ':'.$prefix;
  124. }
  125. $writer->writeAttribute($nsLabel, $uri);
  126. }
  127. }
  128. return $writer;
  129. }
  130. /**
  131. * Recursively build the XML body
  132. *
  133. * @param \XMLWriter $writer XML to modify
  134. * @param Parameter $param API Parameter
  135. * @param mixed $value Value to add
  136. */
  137. protected function addXml(\XMLWriter $writer, Parameter $param, $value)
  138. {
  139. $value = $param->filter($value);
  140. $type = $param->getType();
  141. $name = $param->getWireName();
  142. $prefix = null;
  143. $namespace = $param->getData('xmlNamespace');
  144. if (false !== strpos($name, ':')) {
  145. list($prefix, $name) = explode(':', $name, 2);
  146. }
  147. if ($type == 'object' || $type == 'array') {
  148. if (!$param->getData('xmlFlattened')) {
  149. if ($namespace) {
  150. $writer->startElementNS(null, $name, $namespace);
  151. } else {
  152. $writer->startElement($name);
  153. }
  154. }
  155. if ($param->getType() == 'array') {
  156. $this->addXmlArray($writer, $param, $value);
  157. } elseif ($param->getType() == 'object') {
  158. $this->addXmlObject($writer, $param, $value);
  159. }
  160. if (!$param->getData('xmlFlattened')) {
  161. $writer->endElement();
  162. }
  163. return;
  164. }
  165. if ($param->getData('xmlAttribute')) {
  166. $this->writeAttribute($writer, $prefix, $name, $namespace, $value);
  167. } else {
  168. $this->writeElement($writer, $prefix, $name, $namespace, $value);
  169. }
  170. }
  171. /**
  172. * Write an attribute with namespace if used
  173. *
  174. * @param \XMLWriter $writer XMLWriter instance
  175. * @param string $prefix Namespace prefix if any
  176. * @param string $name Attribute name
  177. * @param string $namespace The uri of the namespace
  178. * @param string $value The attribute content
  179. */
  180. protected function writeAttribute($writer, $prefix, $name, $namespace, $value)
  181. {
  182. if ($namespace) {
  183. $writer->writeAttributeNS($prefix, $name, $namespace, $value);
  184. } else {
  185. $writer->writeAttribute($name, $value);
  186. }
  187. }
  188. /**
  189. * Write an element with namespace if used
  190. *
  191. * @param \XMLWriter $writer XML writer resource
  192. * @param string $prefix Namespace prefix if any
  193. * @param string $name Element name
  194. * @param string $namespace The uri of the namespace
  195. * @param string $value The element content
  196. */
  197. protected function writeElement(\XMLWriter $writer, $prefix, $name, $namespace, $value)
  198. {
  199. if ($namespace) {
  200. $writer->startElementNS($prefix, $name, $namespace);
  201. } else {
  202. $writer->startElement($name);
  203. }
  204. if (strpbrk($value, '<>&')) {
  205. $writer->writeCData($value);
  206. } else {
  207. $writer->writeRaw($value);
  208. }
  209. $writer->endElement();
  210. }
  211. /**
  212. * Create a new xml writer and start a document
  213. *
  214. * @param string $encoding document encoding
  215. *
  216. * @return \XMLWriter the writer resource
  217. * @throws \RuntimeException if the document cannot be started
  218. */
  219. protected function startDocument($encoding)
  220. {
  221. $this->writer = new \XMLWriter();
  222. if (!$this->writer->openMemory()) {
  223. throw new \RuntimeException('Unable to open XML document in memory');
  224. }
  225. if (!$this->writer->startDocument('1.0', $encoding)) {
  226. throw new \RuntimeException('Unable to start XML document');
  227. }
  228. return $this->writer;
  229. }
  230. /**
  231. * End the document and return the output
  232. *
  233. * @param \XMLWriter $writer
  234. *
  235. * @return string the writer resource
  236. */
  237. protected function finishDocument($writer)
  238. {
  239. $writer->endDocument();
  240. return $writer->outputMemory();
  241. }
  242. /**
  243. * Add an array to the XML
  244. *
  245. * @param \XMLWriter $writer
  246. * @param Parameter $param
  247. * @param $value
  248. */
  249. protected function addXmlArray(\XMLWriter $writer, Parameter $param, &$value)
  250. {
  251. if ($items = $param->getItems()) {
  252. foreach ($value as $v) {
  253. $this->addXml($writer, $items, $v);
  254. }
  255. }
  256. }
  257. /**
  258. * Add an object to the XML
  259. *
  260. * @param \XMLWriter $writer
  261. * @param Parameter $param
  262. * @param $value
  263. */
  264. protected function addXmlObject(\XMLWriter $writer, Parameter $param, &$value)
  265. {
  266. $noAttributes = [];
  267. // add values which have attributes
  268. foreach ($value as $name => $v) {
  269. if ($property = $param->getProperty($name)) {
  270. if ($property->getData('xmlAttribute')) {
  271. $this->addXml($writer, $property, $v);
  272. } else {
  273. $noAttributes[] = ['value' => $v, 'property' => $property];
  274. }
  275. }
  276. }
  277. // now add values with no attributes
  278. foreach ($noAttributes as $element) {
  279. $this->addXml($writer, $element['property'], $element['value']);
  280. }
  281. }
  282. /**
  283. * @param $value
  284. * @param Parameter $param
  285. * @param Operation $operation
  286. */
  287. private function visitWithValue(
  288. $value,
  289. Parameter $param,
  290. Operation $operation
  291. ) {
  292. if (!$this->writer) {
  293. $this->createRootElement($operation);
  294. }
  295. $this->addXml($this->writer, $param, $value);
  296. }
  297. }