Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 111 additions & 0 deletions ext/soap/VmSoapClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,105 @@ private static function encodeSoapAnyXml(mixed $inner): string
return self::soapVarRawString($inner);
}

/**
* php-src APACHE_MAP / apache Map — xsi:type local name Map (#32222 / to_zval_map).
*/
private static function isApacheMapElement(\DOMElement $el, ?string $hintType): bool
{
[$local, $ns] = self::xsiTypeNameAndNs($el);
if ('Map' === $local) {
return null === $ns || SoapConstants::APACHE_NAMESPACE === $ns;
}
if ('Map' === $hintType) {
return true;
}

return false;
}

/**
* php-src to_zval_map — item/key/value children → PHP array (#3724, ext/soap/php_encoding.c).
*
* @param list<\DOMElement> $childElements
* @param array<string, string> $classmap
* @param list<array{type_ns: string, type_name: string, from_xml: ?string, to_xml: ?string}> $typemap
* @param array<string, string> $elementTypes
* @param array<string, array<string, string>> $complexTypeFields
*
* @return array<string|int, mixed>|null
*/
private static function decodeApacheMap(
\DOMElement $el,
array $childElements,
bool $singleElementArrays,
array $classmap,
array $typemap,
array $elementTypes,
array $complexTypeFields
): ?array {
if (0 === \count($childElements)) {
if ($el->hasAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'nil')) {
return null;
}

return null;
}
$out = [];
foreach ($childElements as $item) {
$itemLocal = $item->localName ?? $item->nodeName;
if ('item' !== $itemLocal) {
continue;
}
$xmlKey = null;
$xmlValue = null;
foreach ($item->childNodes as $child) {
if (!$child instanceof \DOMElement) {
continue;
}
$ln = $child->localName ?? $child->nodeName;
if ('key' === $ln && null === $xmlKey) {
$xmlKey = $child;
} elseif ('value' === $ln && null === $xmlValue) {
$xmlValue = $child;
}
}
if (null === $xmlKey) {
throw new \SoapFault('Client', "Encoding: Can't decode apache map, missing key");
}
if (null === $xmlValue) {
throw new \SoapFault('Client', "Encoding: Can't decode apache map, missing value");
}
$key = self::domElementToValue(
$xmlKey,
$singleElementArrays,
$classmap,
$typemap,
$elementTypes,
null,
$complexTypeFields
);
$value = self::domElementToValue(
$xmlValue,
$singleElementArrays,
$classmap,
$typemap,
$elementTypes,
null,
$complexTypeFields
);
if (\is_string($key) || \is_int($key)) {
$out[$key] = $value;
} else {
throw new \SoapFault(
'Client',
"Encoding: Can't decode apache map, only Strings or Longs are allowed as keys"
);
}
}

return $out;
}

/**
* php-src to_xml_map — APACHE_MAP item/key/value (#32222).
*
Expand Down Expand Up @@ -3598,6 +3697,18 @@ private static function domElementToValue(
$childElements[] = $child;
}
}
// php-src to_zval_map — APACHE_MAP xsi:type Map → PHP assoc array (#3724).
if (self::isApacheMapElement($el, $hintType)) {
return self::decodeApacheMap(
$el,
$childElements,
$singleElementArrays,
$classmap,
$typemap,
$elementTypes,
$complexTypeFields
);
}
if (0 === \count($childElements)) {
$text = $el->textContent;
if ('' === \trim($text) && $el->hasAttributeNS('http://www.w3.org/2001/XMLSchema-instance', 'nil')) {
Expand Down
42 changes: 42 additions & 0 deletions test/compliance/cases/stdlib/soap_client_apache_map_decode.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
stdlib SoapClient APACHE_MAP response decode to assoc array (#3724, php_encoding.c to_zval_map)
--FILE--
<?php
$dir = sys_get_temp_dir() . '/phpc_soap_map_dec_' . getmypid();
@mkdir($dir);
$resp = $dir . '/r.xml';
file_put_contents($resp, '<?xml version="1.0" encoding="UTF-8"?>'
.'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
.' xmlns:ns1="http://example.com/echo"'
.' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
.' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
.' xmlns:ns2="http://xml.apache.org/xml-soap"'
.' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
.'<SOAP-ENV:Body><ns1:echoResponse>'
.'<return xsi:type="ns2:Map">'
.'<item><key xsi:type="xsd:string">k</key><value xsi:type="xsd:string">v</value></item>'
.'<item><key xsi:type="xsd:int">7</key><value xsi:type="xsd:string">x</value></item>'
.'</return>'
.'</ns1:echoResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>');
$c = new SoapClient(null, [
'location' => $resp,
'uri' => 'http://example.com/echo',
'exceptions' => true,
'style' => SOAP_RPC,
'use' => SOAP_ENCODED,
]);
$out = $c->__soapCall('echo', []);
@unlink($resp);
@rmdir($dir);
echo is_array($out) ? 'array' : gettype($out), "\n";
echo ($out === ['k' => 'v', 7 => 'x']) ? 'match' : 'mismatch', "\n";
var_export($out);
echo "\n";
?>
--EXPECT--
array
match
array (
'k' => 'v',
7 => 'x',
)
35 changes: 35 additions & 0 deletions test/repro/issue_3724_soap_apache_map_decode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/**
* APACHE_MAP response must decode to a PHP assoc array (php-src to_zval_map).
* Host php-soap optional — uses VmSoapClient::decodeResponse via autoload.
*/
require dirname(__DIR__, 2).'/vendor/autoload.php';

use PHPCompiler\ext\soap\VmSoapClient;

if (!\class_exists(\SoapFault::class, false)) {
require dirname(__DIR__, 2).'/ext/soap/bootstrap_soapfault.php';
}

$xml = '<?xml version="1.0" encoding="UTF-8"?>'
.'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
.' xmlns:ns1="http://example.com/echo"'
.' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
.' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
.' xmlns:ns2="http://xml.apache.org/xml-soap"'
.' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
.'<SOAP-ENV:Body><ns1:echoResponse>'
.'<return xsi:type="ns2:Map">'
.'<item><key xsi:type="xsd:string">k</key><value xsi:type="xsd:string">v</value></item>'
.'<item><key xsi:type="xsd:int">7</key><value xsi:type="xsd:string">x</value></item>'
.'</return>'
.'</ns1:echoResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>';

$rm = new ReflectionMethod(VmSoapClient::class, 'decodeResponse');
$rm->setAccessible(true);
$out = $rm->invoke(null, $xml, 'echo');
var_export($out);
echo "\n";
75 changes: 75 additions & 0 deletions test/unit/SoapApacheMapDecodeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPCompiler\ext\soap\VmSoapClient;
use PHPUnit\Framework\TestCase;

/**
* SoapClient APACHE_MAP decode — php-src to_zval_map (#3724, ext/soap/php_encoding.c).
*
* @covers issue #3724
*/
final class SoapApacheMapDecodeTest extends TestCase
{
protected function setUp(): void
{
if (!\class_exists(\SoapFault::class, false)) {
require_once dirname(__DIR__, 2).'/ext/soap/bootstrap_soapfault.php';
}
}

public function testMapDecodesToAssociativeArray(): void
{
$out = $this->decode($this->envelope(
'<return xsi:type="ns2:Map" xmlns:ns2="http://xml.apache.org/xml-soap">'
.'<item><key xsi:type="xsd:string">k</key><value xsi:type="xsd:string">v</value></item>'
.'<item><key xsi:type="xsd:int">7</key><value xsi:type="xsd:string">x</value></item>'
.'</return>'
));
$this->assertIsArray($out);
$this->assertSame(['k' => 'v', 7 => 'x'], $out);
}

public function testEmptyMapIsNull(): void
{
$out = $this->decode($this->envelope(
'<return xsi:type="ns2:Map" xmlns:ns2="http://xml.apache.org/xml-soap"/>'
));
$this->assertNull($out);
}

public function testMissingKeyThrowsSoapFault(): void
{
$this->expectException(\SoapFault::class);
$this->expectExceptionMessage("Encoding: Can't decode apache map, missing key");
$this->decode($this->envelope(
'<return xsi:type="ns2:Map" xmlns:ns2="http://xml.apache.org/xml-soap">'
.'<item><value xsi:type="xsd:string">v</value></item>'
.'</return>'
));
}

private function envelope(string $bodyInner): string
{
return '<?xml version="1.0" encoding="UTF-8"?>'
.'<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
.' xmlns:ns1="http://example.com/echo"'
.' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
.' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
.' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
.'<SOAP-ENV:Body><ns1:echoResponse>'
.$bodyInner
.'</ns1:echoResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>';
}

private function decode(string $xml): mixed
{
$rm = new \ReflectionMethod(VmSoapClient::class, 'decodeResponse');
$rm->setAccessible(true);

return $rm->invoke(null, $xml, 'echo');
}
}
Loading