Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUGFIX: XmlResponseFormatter element names with namespace #18783

Merged
merged 27 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
588770e
XmlResponseFormatter: fixed adding elements
WinterSilence Jul 26, 2021
7bfec0e
Update XmlResponseFormatter.php
WinterSilence Jul 26, 2021
7d704a7
Update XmlResponseFormatter.php
WinterSilence Jul 26, 2021
0dadabb
Update XmlResponseFormatterTest.php
WinterSilence Jul 26, 2021
6dedbe0
Update XmlResponseFormatter.php
WinterSilence Jul 26, 2021
581a0d8
Update XmlResponseFormatterTest.php
WinterSilence Jul 26, 2021
406d744
Update XmlResponseFormatter.php
WinterSilence Jul 26, 2021
baca01d
Update XmlResponseFormatter.php
WinterSilence Jul 26, 2021
e19c839
Update XmlResponseFormatter.php
WinterSilence Aug 3, 2021
bd921ae
Update XmlResponseFormatter.php
WinterSilence Aug 3, 2021
5f2dbf5
Update XmlResponseFormatterTest.php
WinterSilence Aug 3, 2021
7926166
Update XmlResponseFormatterTest.php
WinterSilence Aug 3, 2021
e6b4237
Update XmlResponseFormatter.php
WinterSilence Aug 3, 2021
45933ed
Update framework/web/XmlResponseFormatter.php
samdark Aug 24, 2021
c892646
Update framework/web/XmlResponseFormatter.php
samdark Aug 24, 2021
af21801
Merge branch 'master' into patch-6
samdark Aug 24, 2021
d00f66e
Update XmlResponseFormatter.php
WinterSilence Aug 24, 2021
bf6ac96
Revert unnecessary changes
samdark Aug 24, 2021
e82d80f
Revert more unnecessary changes
samdark Aug 24, 2021
e790610
More reverting
samdark Aug 24, 2021
5bd1de3
Remove unused import
samdark Aug 24, 2021
22abfdc
Add missing use
samdark Aug 24, 2021
015a0ce
Better description
samdark Aug 24, 2021
45c4031
Swap condition back
samdark Aug 24, 2021
1c8d92f
Minor phpdoc fix
samdark Aug 24, 2021
1034267
Separate test
samdark Aug 24, 2021
d82ae4a
Add changelog
samdark Aug 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 57 additions & 35 deletions framework/web/XmlResponseFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
namespace yii\web;

use DOMDocument;
use DOMElement;
use DOMException;
use DOMText;
use Traversable;
use yii\base\Arrayable;
use yii\base\Component;
use yii\helpers\StringHelper;
Expand All @@ -26,19 +25,20 @@
class XmlResponseFormatter extends Component implements ResponseFormatterInterface
{
/**
* @var string the Content-Type header for the response
* @var string the `Content-Type` header for the response
*/
public $contentType = 'application/xml';
/**
* @var string the XML version
* @var string the XML version.
*/
public $version = '1.0';
/**
* @var string the XML encoding. If not set, it will use the value of [[Response::charset]].
*/
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 or index array (URI namespace, tag name).
samdark marked this conversation as resolved.
Show resolved Hide resolved
* If set to false or null then no root tag should be added.
*/
public $rootTag = 'response';
/**
Expand All @@ -52,61 +52,86 @@ class XmlResponseFormatter extends Component implements ResponseFormatterInterfa
*/
public $useTraversableAsArray = true;
/**
* @var bool if object tags should be added
* @var bool if object tags should be added (convert class to tag name)
* @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)
{
$charset = $this->encoding === null ? $response->charset : $this->encoding;
if ($this->encoding === null) {
$this->encoding = $response->charset;
samdark marked this conversation as resolved.
Show resolved Hide resolved
}
if (stripos($this->contentType, 'charset') === false) {
$this->contentType .= '; charset=' . $charset;
$this->contentType .= '; charset=' . $this->encoding;
}
$response->getHeaders()->set('Content-Type', $this->contentType);
if ($response->data !== null) {
$dom = new DOMDocument($this->version, $charset);
if (!empty($this->rootTag)) {
$root = new DOMElement($this->rootTag);
$dom->appendChild($root);
$this->buildXml($root, $response->data);
$this->dom = new DOMDocument($this->version, $this->encoding);
if (empty($this->rootTag)) {
$this->buildXml($this->dom, $response->data);
} else {
$this->buildXml($dom, $response->data);
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);
}
$response->content = $dom->saveXML();
$response->content = $this->dom->saveXML();
}
}

/**
* @param DOMElement $element
* @param mixed $data
* Recursive adds data to XML document.
*
* @param DOMElement|DOMDocument $element the current element
* @param mixed $data the content of current element
*/
protected function buildXml($element, $data)
{
if (is_array($data) ||
($data instanceof \Traversable && $this->useTraversableAsArray && !$data instanceof Arrayable)
if (
is_array($data)
|| (!$data instanceof Arrayable && $data instanceof Traversable && $this->useTraversableAsArray)
) {
foreach ($data as $name => $value) {
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);
samdark marked this conversation as resolved.
Show resolved Hide resolved
}
$child = $this->dom->createElement($name);
$element->appendChild($child);
} else {
$child = $element;
Expand All @@ -121,7 +146,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)));
}
}

Expand All @@ -134,25 +159,23 @@ protected function buildXml($element, $data)
*/
protected function formatScalarValue($value)
{
if ($value === true) {
return 'true';
}
if ($value === false) {
return 'false';
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
if (is_float($value)) {
return StringHelper::floatToString($value);
}

return (string) $value;
}

/**
* Returns element name ready to be used in DOMElement if
* name is not empty, is not int and is valid.
* Returns element name ready to be used in `DOMElement` if name is not empty,
* is not integer and is valid.
*
* Falls back to [[itemTag]] otherwise.
*
* @param mixed $name
* @param mixed $name the original name
* @return string
* @since 2.0.12
*/
Expand All @@ -168,16 +191,15 @@ 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
*/
protected function isValidXmlName($name)
{
try {
new DOMElement($name);
return true;
return $this->dom->createElement($name) !== false;
} catch (DOMException $e) {
return false;
}
Expand Down
15 changes: 13 additions & 2 deletions tests/framework/web/XmlResponseFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function formatScalarDataProvider()
[true, "<response>true</response>\n"],
[false, "<response>false</response>\n"],
['<>', "<response>&lt;&gt;</response>\n"],
['a&b', "<response>a&amp;b</response>\n"],
]);
}

Expand All @@ -57,7 +58,8 @@ public function formatArrayDataProvider()
[[
'a' => 1,
'b' => 'abc',
], "<response><a>1</a><b>abc</b></response>\n"],
'image:loc' => 'url',
], "<response><a>1</a><b>abc</b><image:loc>url</image:loc></response>\n"],
samdark marked this conversation as resolved.
Show resolved Hide resolved
[[
1,
'abc',
Expand All @@ -79,7 +81,7 @@ public function formatArrayDataProvider()
'b:c' => 'b:c',
'a b c' => 'a b c',
'äøñ' => 'äøñ',
], "<response><item>1</item><item>2015-06-18</item><item>b:c</item><item>a b c</item><äøñ>äøñ</äøñ></response>\n"],
], "<response><item>1</item><item>2015-06-18</item><b:c>b:c</b:c><item>a b c</item><äøñ>äøñ</äøñ></response>\n"],
samdark marked this conversation as resolved.
Show resolved Hide resolved
]);
}

Expand Down Expand Up @@ -162,4 +164,13 @@ public function testNoObjectTags()
$formatter->format($this->response);
$this->assertEquals($this->xmlHead . "<response><id>123</id><title>abc</title></response>\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 . "<response><post><id>123</id><title>abc</title></post></response>\n", $this->response->content);
}
}