An API client I have developed works with XML messages and the messages are signed according to the XML Signature Syntax and Processing specification. After a long struggle, I finally got the signatures working.
At this moment I am building the XML with HEREDOC (simply php strings) and with a cleanup, I'd like to create the XML with DOMDocument directly. However, this causes the message to be invalidated by the server.
This is the current setup (server accepts this message when signed):
$xml = <<<EOT
<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
<createDateTimestamp>$timestamp</createDateTimestamp>
<Merchant>
<merchantID>$merchantId</merchantID>
<subID>$subId</subID>
</Merchant>
</DirectoryReq>
EOT;
$document = new DOMDocument();
$document->loadXML($xml);
This is the OO approach (server rejects this message when signed):
$document = new DOMDocument('1.0', 'UTF-8');
$request = $document->createElement('DirectoryReq');
$xmlns = $document->createAttribute('xmlns');
$xmlns->value = 'http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1';
$version = $document->createAttribute('version');
$version->value = '3.3.1';
$request->appendChild($xmlns);
$request->appendChild($version);
$merchant = $document->createElement('Merchant');
$merchant->appendChild($document->createElement('merchantID', $merchantId));
$merchant->appendChild($document->createElement('subID', $subId));
$request->appendChild($document->createElement('createDateTimestamp', $timestamp));
$request->appendChild($merchant);
$document->appendChild($request);
What can cause the difference as such the XML signature is invalidated by the server? The code to sign the message is exactly the same. The server is simply reporting "Invalid electronic signature".
If required I can show more code.
EDIT, more output and comparison of XML generated
To give some more information, this is the output of the first (HEREDOC) xml, generated via $document->saveXml():
<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
<createDateTimestamp>2013-08-10T19:41:20.000Z</createDateTimestamp>
<Merchant>
<merchantID>0020XXXXXX</merchantID>
<subID>0</subID>
</Merchant>
</DirectoryReq>
This is the output ($document->saveXML()) for the second (direct DOMDocument generation) method:
<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
<createDateTimestamp>2013-08-10T19:41:20.000Z</createDateTimestamp>
<Merchant>
<merchantID>0020XXXXXX</merchantID>
<subID>0</subID>
</Merchant>
</DirectoryReq>
In php, var_dump() gives the exact same string length. If I compare both strings (=== obviously) they are the same. Comparing both objects, then they are not the same.
Signing example
Signing occurs with the library xmlseclibs with this code (NB. both types are signed the same way!):
public function sign(DOMDocument $document, $fingerprint, $keyfile, $passphrase = null)
{
$dsig = new XMLSecurityDSig();
$dsig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$dsig->addReference($document, XMLSecurityDSig::SHA256,
array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'),
array('force_uri' => true)
);
$key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
if ($passphrase !== null) {
$key->passphrase = $passphrase;
}
$key->loadKey($keyfile, true);
$dsig->sign($key);
$dsig->addKeyInfoAndName($fingerprint);
$dsig->appendSignature($document->documentElement);
}
If I dump the XML after it's signed, the <DigestValue> and <SignatureValue> values are different. So the server is correct the signature is invalid, but I cannot come up with a clue why method A works and B not.
You are overwriting $merchant when you create the Merchant element, so just rename the variable
$merchantElement = $document->createElement('Merchant');
I have now solved it by exporting and importing the XML again. It's quite ugly, but allows me to flexibly handle the DOMNodes.
protected function repairDOMDocument(DOMDocument $document)
{
$xml = $document->saveXML();
$document = new DOMDocument;
$document->loadXML($xml);
return $document;
}
If there is a suggestion how to stop doing this, I am pleased to hear so.
Related
The issue: I am sending SOAP request to 3rd-party server and inside one of my tags I have all "<" ">" replaced with html-entities < >
The info:
Lets say I have this class(simplified and pseudocoded for clarity):
Class SoapSender {
private $session = 'sessionHash';
private $soap = new SoapClient(/* settings */);
public function create() {
try {
$agr = [];
$agr['SessionToken'] = $this->session;
$agr['Document'] = $this->prepareDocument();
$request = $this->soap->createRequest($agr); // 3rd-party call
} catch (SoapFault $f) {
/* handle exception */
}
}
The documentation of 3rd-party says that <Document> should be XML string so I do this:
private function prepareDocument() {
$arr = [
/* Create all sub-tags of <Document> */
];
$xml = Formatter::make($arr, 'array')->toXml();
// this operations required for valid structure
$xml = str_replace(array('<?xml version="1.0" encoding="utf-8"?>','<xml>','</xml>'), '', $xml);
$xml = str_replace(['Subject_1', 'Subject_2'], 'Subject', $xml);
$xml = str_replace(['Period_1', 'Period_2'], 'Period', $xml);
$xml = preg_replace('/Store_[1-9]/', 'Store', $xml);
return $xml;
Formatter is SoapBox/laravel-formatter.
Soo, when I send request and recieve response with errors I dump $this->soap->__getLastRequest() and see that:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://someurl.com">
<SOAP-ENV:Body><ns1:CreateRequest>
<Document>
<General><Product>Bread</Product><DateBeg>2016-04-04T00:00:00</DateBeg><DateEnd>2017-04-03</DateEnd><.....
...........blah-blah....
</Document>
<SessionToken>SessionTokeon</SessionToken></ns1:CreateRequest></SOAP-ENV:Body></SOAP-ENV:Envelope>
What I have so far: debuging prepareDocument says that I have valid xml string in utf-8 with all tags intact, so encoding them to html-entities happens on SOAP request. Only the Document tag gets encoded, others are ok.
All my other services that use same aproach but different 3rd-party works perfect. Sending this request (but decoded back to normal) by SOAP UI works as it should. All happens inside laravel project.
The issue again: I am sending request to 3rd-party server and inside one of my tags I have all "<" ">" replaced with html-entities < >
The question: What should I do to fix this issue?
I'm sending the following XML to an api using cURL:
$xml = "<request type='auth' timestamp='$timestamp'>
<merchantid>$merchantid</merchantid>
<account>$account</account>
<orderid>$orderid</orderid>
<amount currency='$currency'>$amount</amount>
<card>
<number>$cardnumber</number>
<expdate>$expdate</expdate>
<type>$cardtype</type>
<chname>$cardname</chname>
</card>
<sha1hash>$sha1hash</sha1hash>
</request>";
What is the best way to avoid hard coding this XML? I was thinking of using XMLWriter but seems strange as it won't be changing.
Should I use a template? Or generate it using XMLWriter / Simple XML?
As I mentioned in the comments, there's not necessarily a right answer to this but I recently had to write a project around an XML API Feed as well. I decided to go with XMLWriter and it's still very easy to interchange into others easily by using their respected .loadXML() functions.
class SomeApi extends XMLwriter {
public function __construct() {
$this->openMemory();
$this->setIndent( true );
$this->setIndentString ( " " );
$this->startDocument( '1.0', 'UTF-8', 'no' );
$this->startElement( 'root' );
}
public function addNode( $Name, $Contents ) {
$this->startElement( $Name );
$this->writeCData( $Contents );
$this->endElement();
}
public function output() {
$this->endElement();
$this->endDocument();
}
//Returns a String of Xml.
public function render() {
return $this->outputMemory();
}
}
$newRequest = new SomeApi();
$newRequest->addNode( 'some', 'Some Lots of Text' );
$Xml = $newRequest->render();
I think it's a nice clean way writing an XML Feed in PHP, furthermore as you can add internal functions such as:
$this->addHeader();
private function addHeader() {
$this->addNode( 'login', 'xxxxx' );
$this->addNode( 'password', 'xxxxx' );
}
Which then appends nodes that you'll use over & over again. Then if you suddenly need to use a DOMDocument object for example (As I needed too for XSL).
$Dom = new DOMDocument();
$Dom->loadXML( $Xml );
Should I use a template?
You actually already did use a template here.
Or generate it using XMLWriter / Simple XML?
XMLWriter and also SimpleXMLElement are components that allow you to create XML easily. For your specific case I'd use SimpleXML for a start:
$xml = new SimpleXMLElement('<request type="auth"/>');
$xml['timestamp'] = $timestamp;
$xml->merchantid = $merchantid;
$xml->account = $account;
$xml->orderid = $orderid;
$xml->addChild('amount', $amount)['currency'] = $currency;
$card = $xml->addChild('card');
$card->number = $cardnumber;
$card->expdate = $expdate;
$card->type = $cardtype;
$card->chname = $cardname;
$xml->sha1hash = $sha1hash;
See that the XML is not hardcoded any longer, only the names used are. The SimpleXML library takes care to create the XML (demo, here the output is beautified for better readability):
<?xml version="1.0"?>
<request type="auth" timestamp="">
<merchantid></merchantid>
<account></account>
<orderid></orderid>
<amount currency=""/>
<card>
<number></number>
<expdate></expdate>
<type></type>
<chname></chname>
</card>
<sha1hash></sha1hash>
</request>
Thanks to the library, the output is always valid XML and you don't need to care about the details here. You can further simplify it by wrapping it more, but I don't think this is of much use with your very little XML you have here.
I want to integrate an XSL file in an XML string gived me by php CURL command.
I tryed this
$output = XML gived me by curl option;
$hotel = simplexml_load_string($output);
$hotel->addAttribute('?xml-stylesheet type=”text/xsl” href=”css/stile.xsl”?');
echo $hotel->asXML();
Doing this when I see the XML on browser, I receive the file without the stylesheet.
Where is my error?
A SimpleXMLElement does not allow you by default to create and add a Processing Instruction (PI) to a node. However the sister library DOMDocument allows this. You can marry the two by extending from SimpleXMLElement and create a function to provide that feature:
class MySimpleXMLElement extends SimpleXMLElement
{
public function addProcessingInstruction($target, $data = NULL) {
$node = dom_import_simplexml($this);
$pi = $node->ownerDocument->createProcessingInstruction($target, $data);
$result = $node->appendChild($pi);
return $this;
}
}
This then is easy to use:
$output = '<hotel/>';
$hotel = simplexml_load_string($output, 'MySimpleXMLElement');
$hotel->addProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="style.xsl"');
$hotel->asXML('php://output');
Exemplary output (beautified):
<?xml version="1.0"?>
<hotel>
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
</hotel>
Another way is to insert an XML chunk to a simplexml element: "PHP SimpleXML: insert node at certain position" or "Insert XML into a SimpleXMLElement".
After hours of searching, finding similar threads and still not being able to get it to work I've resorted to posting my specific problem. I'm getting a SOAP encoded XML response from a server that i want to use SimpleXMLElement() on, but i'm having a real hard time establishing a base path to work from.
I've tried two different methods:
xpath():
public function XMLParseUserData($xml)
{
$ActivityData = new SimpleXMLElement($xml);
$ActivityData->registerXPathNamespace("ns", "http://webservices.website.net/");
$basePath = $ActivityData->xpath('//ns:GetUserActivityDataResult/ActArray');
foreach ($basePath->ACT as $userActivity)
{
$this->uGUID = $userActivity->UserGUID;
echo $this->uGUID."<br />";
}
}
children():
public function XMLParseUserData($xml)
{
$ActivityData = new SimpleXMLElement($xml);
$basePath = $ActivityData->children('soap',true)->Body->GetUserActivityDataResponse->GetUserActivityDataResult->ActArray->ACT;
foreach ($basePath as $userActivity)
{
$this->uGUID = $userActivity->UserGUID;
echo $this->uGUID."<br />";
}
}
The XML response:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetUserActivityDataResponse xmlns="http://webservices.website.net/">
<GetUserActivityDataResult>
<ResponseCode>SUCCESS</ResponseCode>
<FailedUserCount>0</FailedUserCount>
<ActCount>1</ActCount>
<ActArray>
<ACT>
<UserGUID>0dc299ba-XXXX-XXXX-XXXX-7ca097d51eb6</UserGUID>
<ActDataCount>15</ActDataCount>
<ActData>
<ACT_DATA>
<Start>2012-03-05T08:40:00</Start>
<End>2012-03-05T09:00:00</End>
<SourceCount>1</SourceCount>
<SourceData>
<ACT_SRC_DATA>
<Source>ACTIPED</Source>
<TypeCount>3</TypeCount>
<TypeData>
<ACT_TYPE_DATA>
<Type>WALK</Type>
<S>40</S>
<C>2</C>
<D>20</D>
<T>16</T>
</ACT_TYPE_DATA>
<ACT_TYPE_DATA>
<Type>RUN</Type>
<S>20</S>
<C>2</C>
<D>20</D>
<T>10</T>
</ACT_TYPE_DATA>
<ACT_TYPE_DATA>
<Type>OTHER</Type>
<S>0</S>
<C>0</C>
<D>0</D>
<T>28</T>
</ACT_TYPE_DATA>
</TypeData>
<MetricCount>0</MetricCount>
</ACT_SRC_DATA>
</SourceData>
</ACT_DATA>
</ActData>
</ACT>
</ActArray>
<AsOfServerTimeGMT>2012-03-06T16:41:41.513</AsOfServerTimeGMT>
</GetUserActivityDataResult>
</GetUserActivityDataResponse>
</soap:Body>
</soap:Envelope>
Neither method works and both leave me with the same error:
Warning: Invalid argument supplied for foreach() in /c08/domains/dev.mysite.com/html/class/XMLParse.class.php on line 29
It says:
<GetUserActivityDataResponse xmlns="http://webservices.website.net/">
and you are trying to do
$basePath = $soapEnvelope
->children('soap', true)
->Body
->GetUserActivityDataResponse
…
which means you try to get <soap:GetUserActivityDataResponse> which obviously doesnt exist. You have to do (demo)
$basePath = $soapEnvelope
->children('soap', true)
->Body
->children('http://webservices.website.net/')
->GetUserActivityDataResponse
->GetUserActivityDataResult
->ActArray
->ACT;
Actually, you could just do ->children() to jump back to the default namespace, but I find providing the namespace explicitly somewhat clearer. Your choice.
Your XPath fails because you didn't specify the namespace for ActArray. Also, when xpath() is successful, it returns an array of SimpleXmlElements. You tried array->ACT, which doesn't work because an array is not an object. The first ActArray is in $basePath[0]. So you have to adjust the code to
$basePath = $soapEnvelope->xpath('//ns:GetUserActivityDataResult/ns:ActArray');
foreach ($basePath[0]->ACT as $userActivity) {
…
To get the ACT elements directly, change the XPath to
//ns:GetUserActivityDataResult/ns:ActArray/ns:ACT
is there an option with DomDocument to remove the first line:
<?xml version="1.0" encoding="UTF-8"?>
The class instantiation automatically adds it to the output, but is it possible to get rid of it?
I think using DOMDocument is a universal solution for valid XML files:
If you have XML already loaded in a variable:
$t_xml = new DOMDocument();
$t_xml->loadXML($xml_as_string);
$xml_out = $t_xml->saveXML($t_xml->documentElement);
For XML file from disk:
$t_xml = new DOMDocument();
$t_xml->load($file_path_to_xml);
$xml_out = $t_xml->saveXML($t_xml->documentElement);
This comment helped: http://www.php.net/manual/en/domdocument.savexml.php#88525
If you want to output HTML, use the saveHTML() function. It automatically avoids a whole lot of XML idiom and handles closed/unclosed HTML idiom properly.
If you want to output XML you can use the fact that DOMDocument is a DOMNode (namely: '/' in XPath expression), thus you can use DOMNode API calls on it to iterate over child nodes and call saveXML() on each child node. This does not output the XML declaration, and it outputs all other XML content properly.
Example:
$xml = get_my_document_object();
foreach ($xml->childNodes as $node) {
echo $xml->saveXML($node);
}
For me, none of the answers above worked:
$dom = new \DOMDocument();
$dom->loadXXX('<?xml encoding="utf-8" ?>' . $content); // loadXML or loadHTML
$dom->saveXML($dom->documentElement);
The above didn't work for me if I had partial HTML, e.g.
<p>Lorem</p>
<p>Ipsum</p>
As it then removed the everything after <p>Lorem</p>.
The only solution that worked for me was:
foreach ($doc->childNodes as $xx) {
if ($xx instanceof \DOMProcessingInstruction) {
$xx->parentNode->removeChild($xx);
}
}
I had the same problem, but I am using symfony/serializer for XML creation. If you also want to achieve this with Symfony serializer you can do in this way:
$encoder = new \Symfony\Component\Serializer\Encoder\XmlEncoder();
$encoder->encode($nodes[$rootNodeName], 'xml', [
XmlEncoder::ROOT_NODE_NAME => $rootNodeName,
XmlEncoder::ENCODING => $encoding,
XmlEncoder::ENCODER_IGNORED_NODE_TYPES => [
XML_PI_NODE, //this flag is the solution
],
]);
You can use output buffering to remove it. A bit of a hack but it works.
ob_start();
// dom stuff
$output = ob_get_contents();
ob_end_clean();
$clean = preg_replace("/(.+?\n)/","",$output);