diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 4b03409c99d..2db7f65f261 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -8,7 +8,8 @@ Yii Framework 2 Change Log - Enh #18826: Add ability to turn the sorting off for a clicked column in GridView with multisort (ditibal) - Bug #18646: Remove stale identity data from session if `IdentityInterface::findIdentity()` returns `null` (mikehaertl) - Bug #18832: Fix `Inflector::camel2words()` adding extra spaces (brandonkelly) - +- Enh #18783: Add support for URI namespaced tags in `XmlResponseFormatter` (WinterSilence, samdark) +- Enh #18783: Add `XmlResponseFormatter::$objectTagToLowercase` option to lowercase object tags (WinterSilence, samdark) 2.0.43 August 09, 2021 ---------------------- diff --git a/framework/web/XmlResponseFormatter.php b/framework/web/XmlResponseFormatter.php index 40e9776bc72..f55d5da3f04 100644 --- a/framework/web/XmlResponseFormatter.php +++ b/framework/web/XmlResponseFormatter.php @@ -10,7 +10,6 @@ use DOMDocument; use DOMElement; use DOMException; -use DOMText; use yii\base\Arrayable; use yii\base\Component; use yii\helpers\StringHelper; @@ -38,7 +37,10 @@ class XmlResponseFormatter extends Component implements ResponseFormatterInterfa */ public $encoding; /** - * @var string the name of the root element. If set to false, null or is empty then no root tag should be added. + * @var string|string[]|false the name of the root element. If set to false, null or is empty then no root tag + * should be added. + * + * Since 2.0.43 URI namespace could be specified by passing `[namespace, tag name]` array. */ public $rootTag = 'response'; /** @@ -52,14 +54,26 @@ class XmlResponseFormatter extends Component implements ResponseFormatterInterfa */ public $useTraversableAsArray = true; /** - * @var bool if object tags should be added + * @var bool if object class names should be used as tag names * @since 2.0.11 */ public $useObjectTags = true; + /** + * @var bool if true, converts object tags to lowercase, `$useObjectTags` must be enabled + * @since 2.0.43 + */ + public $objectTagToLowercase = false; + + /** + * @var DOMDocument the XML document, serves as the root of the document tree + * @since 2.0.43 + */ + protected $dom; /** * Formats the specified response. + * * @param Response $response the response to be formatted. */ public function format($response) @@ -70,21 +84,27 @@ public function format($response) } $response->getHeaders()->set('Content-Type', $this->contentType); if ($response->data !== null) { - $dom = new DOMDocument($this->version, $charset); + $this->dom = new DOMDocument($this->version, $charset); if (!empty($this->rootTag)) { - $root = new DOMElement($this->rootTag); - $dom->appendChild($root); + if (is_array($this->rootTag)) { + $root = $this->dom->createElementNS($this->rootTag[0], $this->rootTag[1]); + } else { + $root = $this->dom->createElement($this->rootTag); + } + $this->dom->appendChild($root); $this->buildXml($root, $response->data); } else { - $this->buildXml($dom, $response->data); + $this->buildXml($this->dom, $response->data); } - $response->content = $dom->saveXML(); + $response->content = $this->dom->saveXML(); } } /** - * @param DOMElement $element - * @param mixed $data + * Recursively adds data to XML document. + * + * @param DOMElement|DOMDocument $element current element + * @param mixed $data content of the current element */ protected function buildXml($element, $data) { @@ -95,18 +115,22 @@ protected function buildXml($element, $data) if (is_int($name) && is_object($value)) { $this->buildXml($element, $value); } elseif (is_array($value) || is_object($value)) { - $child = new DOMElement($this->getValidXmlElementName($name)); + $child = $this->dom->createElement($this->getValidXmlElementName($name)); $element->appendChild($child); $this->buildXml($child, $value); } else { - $child = new DOMElement($this->getValidXmlElementName($name)); + $child = $this->dom->createElement($this->getValidXmlElementName($name)); + $child->appendChild($this->dom->createTextNode($this->formatScalarValue($value))); $element->appendChild($child); - $child->appendChild(new DOMText($this->formatScalarValue($value))); } } } elseif (is_object($data)) { if ($this->useObjectTags) { - $child = new DOMElement(StringHelper::basename(get_class($data))); + $name = StringHelper::basename(get_class($data)); + if ($this->objectTagToLowercase) { + $name = strtolower($name); + } + $child = $this->dom->createElement($name); $element->appendChild($child); } else { $child = $element; @@ -121,7 +145,7 @@ protected function buildXml($element, $data) $this->buildXml($child, $array); } } else { - $element->appendChild(new DOMText($this->formatScalarValue($data))); + $element->appendChild($this->dom->createTextNode($this->formatScalarValue($data))); } } @@ -152,7 +176,7 @@ protected function formatScalarValue($value) * * Falls back to [[itemTag]] otherwise. * - * @param mixed $name + * @param mixed $name the original name * @return string * @since 2.0.12 */ @@ -168,7 +192,7 @@ protected function getValidXmlElementName($name) /** * Checks if name is valid to be used in XML. * - * @param mixed $name + * @param mixed $name the name to test * @return bool * @see http://stackoverflow.com/questions/2519845/how-to-check-if-string-is-a-valid-xml-element-name/2519943#2519943 * @since 2.0.12 @@ -176,8 +200,7 @@ protected function getValidXmlElementName($name) protected function isValidXmlName($name) { try { - new DOMElement($name); - return true; + return $this->dom->createElement($name) !== false; } catch (DOMException $e) { return false; } diff --git a/tests/framework/web/XmlResponseFormatterTest.php b/tests/framework/web/XmlResponseFormatterTest.php index 947c50af004..20a63f81769 100644 --- a/tests/framework/web/XmlResponseFormatterTest.php +++ b/tests/framework/web/XmlResponseFormatterTest.php @@ -46,6 +46,7 @@ public function formatScalarDataProvider() [true, "true\n"], [false, "false\n"], ['<>', "<>\n"], + ['a&b', "a&b\n"], ]); } @@ -58,6 +59,10 @@ public function formatArrayDataProvider() 'a' => 1, 'b' => 'abc', ], "1abc\n"], + [ + ['image:loc' => 'url'], + "url\n" + ], [[ 1, 'abc', @@ -79,7 +84,7 @@ public function formatArrayDataProvider() 'b:c' => 'b:c', 'a b c' => 'a b c', 'äøñ' => 'äøñ', - ], "12015-06-18b:ca b c<äøñ>äøñ\n"], + ], "12015-06-18b:ca b c<äøñ>äøñ\n"], ]); } @@ -162,4 +167,13 @@ public function testNoObjectTags() $formatter->format($this->response); $this->assertEquals($this->xmlHead . "123abc\n", $this->response->content); } + + public function testObjectTagToLowercase() + { + $formatter = $this->getFormatterInstance(['objectTagToLowercase' => true]); + + $this->response->data = new Post(123, 'abc'); + $formatter->format($this->response); + $this->assertEquals($this->xmlHead . "123abc\n", $this->response->content); + } }