fix: strip invalid XML characters from responses
This commit is contained in:
1 parent
b047465339
commit
e9766dbdce
3 files changed
+147
-1
No files matched your search
@@ -6,6 +6,7 @@ use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use App\BusProNet\MessageType;
|
||||
use App\BusProNet\Utility\XmlCharacterFilter;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\Service\XmlDumpWriter;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -208,8 +209,18 @@ class XmlChannel
|
||||
*/
|
||||
private function parseResponse(MessageType $type, string $xml, array $parserArgs, string $requestId, bool $dumpOnFailure = true): mixed
|
||||
{
|
||||
// The dump keeps the raw response; only the parser sees the filtered one.
|
||||
$filtered = XmlCharacterFilter::strip($xml, $removed);
|
||||
if ($removed > 0) {
|
||||
$this->logger->warning('Removed characters not allowed in XML from API response', [
|
||||
'request_id' => $requestId,
|
||||
'type' => $type->value,
|
||||
'removed' => $removed,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->responseParser->parseXmlString($type, $xml, $parserArgs);
|
||||
return $this->responseParser->parseXmlString($type, $filtered, $parserArgs);
|
||||
} catch (ResponseParserException $e) {
|
||||
// Keep the evidence even outside debug mode: without the raw response a parse
|
||||
// failure is not diagnosable after the fact. app:cleanup:xml-dumps prunes it.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Utility;
|
||||
|
||||
/**
|
||||
* Removes characters XML 1.0 does not allow from a BusPro response before it is parsed.
|
||||
*
|
||||
* BusPro escapes whatever a field holds into a character reference without checking it,
|
||||
* so a NUL-padded value comes out as `�`. libxml rejects that as fatal and the whole
|
||||
* response fails to parse, not just the field. The same applies to the raw control bytes.
|
||||
*/
|
||||
final class XmlCharacterFilter
|
||||
{
|
||||
/**
|
||||
* @param-out int $count the number of characters removed
|
||||
*/
|
||||
public static function strip(string $xml, ?int &$count = null): string
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
$xml = preg_replace_callback(
|
||||
'/&#(x[0-9a-f]+|[0-9]+);/i',
|
||||
static function (array $match) use (&$count): string {
|
||||
$reference = $match[1];
|
||||
$codePoint = 'x' === strtolower($reference[0]) ? hexdec(substr($reference, 1)) : (int) $reference;
|
||||
|
||||
if (self::isAllowed($codePoint)) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
++$count;
|
||||
|
||||
return '';
|
||||
},
|
||||
$xml,
|
||||
);
|
||||
|
||||
// Control bytes never occur inside a UTF-8 multibyte sequence, so a byte-wise match is safe.
|
||||
$xml = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $xml, -1, $rawCount);
|
||||
|
||||
$count += $rawCount;
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Char production of XML 1.0.
|
||||
*/
|
||||
private static function isAllowed(int|float $codePoint): bool
|
||||
{
|
||||
return 0x9 === $codePoint
|
||||
|| 0xA === $codePoint
|
||||
|| 0xD === $codePoint
|
||||
|| ($codePoint >= 0x20 && $codePoint <= 0xD7FF)
|
||||
|| ($codePoint >= 0xE000 && $codePoint <= 0xFFFD)
|
||||
|| ($codePoint >= 0x10000 && $codePoint <= 0x10FFFF);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user