diff --git a/system/Format/Exceptions/FormatException.php b/system/Format/Exceptions/FormatException.php
index cde333b1292c..7c7e2168c69a 100644
--- a/system/Format/Exceptions/FormatException.php
+++ b/system/Format/Exceptions/FormatException.php
@@ -57,6 +57,17 @@ public static function forInvalidMime(string $mime)
return new static(lang('Format.invalidMime', [$mime]));
}
+ /**
+ * Thrown in XMLFormatter when a value cannot be converted
+ * to a string.
+ *
+ * @return static
+ */
+ public static function forInvalidXMLValue(string $type)
+ {
+ return new static(lang('Format.invalidXMLValue', [$type]));
+ }
+
/**
* Thrown on XMLFormatter when the `simplexml` extension
* is not installed.
diff --git a/system/Format/XMLFormatter.php b/system/Format/XMLFormatter.php
index 5ef2908858b7..3a2e8a6f20a9 100644
--- a/system/Format/XMLFormatter.php
+++ b/system/Format/XMLFormatter.php
@@ -16,6 +16,7 @@
use CodeIgniter\Format\Exceptions\FormatException;
use Config\Format;
use SimpleXMLElement;
+use Stringable;
/**
* XML data formatter
@@ -68,7 +69,11 @@ protected function arrayToXML(array $data, &$output)
$subnode = $output->addChild("{$key}");
$this->arrayToXML($value, $subnode);
} else {
- $output->addChild("{$key}", htmlspecialchars("{$value}"));
+ if (is_object($value) && ! $value instanceof Stringable) {
+ throw FormatException::forInvalidXMLValue($value::class);
+ }
+
+ $output->addChild("{$key}", htmlspecialchars((string) $value));
}
}
}
@@ -98,6 +103,8 @@ protected function normalizeXMLTag($key)
$key = preg_replace("/[^{$validName}-]+/u", '', $key);
$key = preg_replace("/^[^{$startChar}]+/u", 'item$0', $key);
- return preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word
+ $key = preg_replace('/^(xml).*/iu', 'item$0', $key); // XML is a reserved starting word
+
+ return $key === '' ? 'item' : $key;
}
}
diff --git a/system/Language/en/Format.php b/system/Language/en/Format.php
index 7d792e2b6128..1f217798afe3 100644
--- a/system/Language/en/Format.php
+++ b/system/Language/en/Format.php
@@ -16,5 +16,6 @@
'invalidFormatter' => '"{0}" is not a valid Formatter class.',
'invalidJSON' => 'Failed to parse JSON string. Error: {0}',
'invalidMime' => 'No Formatter defined for mime type: "{0}".',
+ 'invalidXMLValue' => 'Cannot convert the value of type "{0}" to a string for XML output.',
'missingExtension' => 'The SimpleXML extension is required to format XML.',
];
diff --git a/tests/system/Format/XMLFormatterTest.php b/tests/system/Format/XMLFormatterTest.php
index 107418a5d68c..f86aa95d1feb 100644
--- a/tests/system/Format/XMLFormatterTest.php
+++ b/tests/system/Format/XMLFormatterTest.php
@@ -13,10 +13,13 @@
namespace CodeIgniter\Format;
+use CodeIgniter\Format\Exceptions\FormatException;
use CodeIgniter\Test\CIUnitTestCase;
use DOMDocument;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
+use stdClass;
+use Stringable;
/**
* @internal
@@ -152,6 +155,51 @@ public static function provideValidatingInvalidTags(): iterable
];
}
+ public function testEmptyTagNameAfterSanitization(): void
+ {
+ $data = [
+ '!@#' => 'value',
+ ];
+
+ $expected = <<<'EOH'
+
+ - value
+
+ EOH;
+
+ $this->assertSame($expected, $this->xmlFormatter->format($data));
+ }
+
+ public function testStringableObjectValue(): void
+ {
+ $value = new class () implements Stringable {
+ public function __toString(): string
+ {
+ return 'bar';
+ }
+ };
+
+ $data = [
+ 'foo' => $value,
+ ];
+
+ $expected = <<<'EOH'
+
+ bar
+
+ EOH;
+
+ $this->assertSame($expected, $this->xmlFormatter->format($data));
+ }
+
+ public function testNonStringableObjectValueThrows(): void
+ {
+ $this->expectException(FormatException::class);
+ $this->expectExceptionMessage('Cannot convert the value of type "stdClass" to a string for XML output.');
+
+ $this->xmlFormatter->format(['foo' => new stdClass()]);
+ }
+
public function testDeepNestedArrayToXml(): void
{
$data = [
diff --git a/user_guide_src/source/changelogs/v4.7.6.rst b/user_guide_src/source/changelogs/v4.7.6.rst
index 360221806c4f..029fa79286e5 100644
--- a/user_guide_src/source/changelogs/v4.7.6.rst
+++ b/user_guide_src/source/changelogs/v4.7.6.rst
@@ -18,6 +18,8 @@ BREAKING
Message Changes
***************
+- Added the ``Format.invalidXMLValue`` language string.
+
*******
Changes
*******
@@ -31,6 +33,11 @@ Bugs Fixed
**********
- **Commands:** Fixed a bug where generator commands did not sort the imports of a template with CRLF line endings.
+- **Format:** Fixed ``XMLFormatter`` so that an array key that becomes empty after
+ sanitization no longer triggers a ``ValueError``; the fallback tag name ``item``
+ is used instead. Values that cannot be converted to a string, such as objects
+ not implementing ``Stringable``, now throw a ``FormatException`` instead of a
+ fatal ``Error``.
- **Security:** Fixed a bug where a valid JSON request body that is not an object
(for example a JSON array) was corrupted after CSRF verification when the
token was sent via the ``X-CSRF-TOKEN`` header.