I have written a PHP SOAP Service that accepts Basic Authentication credentials as outlined at http://www.whitemesa.com/soapauth.html. I did this by defining a method BasicAuth inside the handler class of the SOAPServer instance. This all works fine.
However, when authentication fails for some reason (incorrect username, no BasicAuth header in the request) I'd like to include a BasicChallenge header in my response, like this:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header>
<h:BasicChallenge xmlns:h="http://soap-authentication.org/basic/2001/10/"
SOAP-ENV:mustUnderstand="1">
<Realm>Realm</Realm>
</h:BasicChallenge>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<SOAP-ENV:Fault>
<faultcode>SOAP-ENV:Client</faultcode>
<faultstring>Authentication failed</faultstring>
</SOAP-ENV:Fault>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The following code does not work (the header is not added to the response).
$soapServer->addSoapHeader(new SoapHeader("http://soap-authentication.org/basic/2001/10/", "BasicChallenge", array("Realm" => "Realm"), true));
throw new SoapFault("Client", "Authentication Failed");
Calling $soapServer->fault() instead of throw new SoapFault does not make a difference.
I've tried constructing the Fault object myself, and returning that as a regular response, but I was unable to get PHP to send a well-formed response.
Thanks in advance.
This was 2013 and I actually have exactly the same problem. Well, it 's 2019 and PHP 's current version is 7.3 and 7.4 is coming up to us with big steps. Unfortunately the SoapServer class ignores soap headers completely in a SoapFault case.
I 've written a small workaround to manipulate the XML response of the soap server. For everone who 's having the same issue, here 's a small example, how to solve it.
1. Initialise your Soap Server
$server = new SoapServer($wsdl, $options);
ob_start();
$server->setObject($service);
$server->handle();
$response = ob_get_contents();
ob_end_clean();
Actually we 're doing a simple initialization. Instead of simple returning the content we intercept the xml response with the output buffering functions. The result of the is a xml string in $response.
2. Find out if the reponse is a fault
Actually the SoapServer class is adding soap headers, if it was a valid response. We have to find out, if the response is a fault.
$doc = new DOMDocument();
$doc->loadXML($response);
$xpath = new DOMXPath($doc);
$isFault = $xpath->query('//*[local-name()="Fault"]')->length;
Just load the response xml string into the DOMDocument class. From now on we are able to access xml elements with DOM functions. For better handling I 'm using the DOMXPath class. Of course it is also possible to determine the XML nodes with the DOMDocument class. The $isFault variable is useful for the next step.
3. In fault case set a soap header
Unfortunately there is no fancy addSoapHeader function for simply setting a soap header. We have to do it manually in this case.
if ($isFault) {
$header = $doc->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'Header');
$body = $doc->getElementsByTagNameNS('http://schemas.xmlsoap.org/soap/envelope/', 'Body')->item(0);
$realm = $doc->createElementNS('http://soap-authentication.org/basic/2001/10/', 'h:Realm');
$basicChallenge = $doc->createElementNS('http://soap-authentication.org/basic/2001/10/', 'h:BasicChallenge');
$basicChallenge->appendChild($realm);
$header->appendChild($basicChallenge);
$doc->documentElement->insertBefore($header, $body);
}
$xmlResponse = $doc->saveXML($doc->documentElement);
header('Content-Length: ' . strlen($xmlResponse));
echo $xmlResponse;
exit();
In a fault case we create a header element and add all the child nodes we need. If averything was appended insert the header before the body, save the new xml structure in a string and echo it.
Hope this helps a bit.
Related
I'm trying to parse XML to get "text" message:
<?xml version="1.0" encoding="UTF-8"?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope">
<faultcode>S:Server</faultcode>
<faultstring>Error saving JobsReport</faultstring>
<detail>
<ns2:ErrorMsg xmlns:ns2="http://www.testX.com.pl/wsdl/sdis-emm">
<Error code="20" text="More than one row with the given identifier was found: 389, for class: ekt.bean.sdis.RepPerson" />
</ns2:ErrorMsg>
</detail>
</S:Fault>
</S:Body>
</S:Envelope>
but SimpleXMLElement() and simplexml_load_file() returns only empty object. When I remove "S:" is better, but not ok.
registerXPathNamespace doesn't help.
Help, thx.
Looks like a Soap response. PHP 's own SoapClient object deals automatically with this XML and delivers objects as responses as described in the functions and types definitions of the webservice. Here 's a small illustrative example:
try {
$client = new SoapClient($wsdl, $options);
$response = $client->functionName($params);
var_dump($response);
// response would be an object with members as described by the webservice
} catch (SoapFault $e) {
// prints error message
echo $e->getMessage();
}
So for more comfort you should definitely take a look on SoapClient in the PHP manual. SoapClient handles all the XML stuff automatically so you will not get in touch with it.
If you still want to handle the XML requests and responses on yourself, you can do it with PHP 's own DomDocument object. Here 's a small illustrative example:
$dom = new DomDocument();
$dom->loadXml($xmlResponse);
foreach ($dom->getElementsByTagNameNS('http://www.testX.com.pl/wsdl/sdis-emm', 'Error') as $element) {
echo $element->getAttribute('text');
}
This is the more complicated way, because SoapClient throws a SoapFault, if an error occurs, which you can easiely catch and do whatever you want with it.
I am new to SOAP WSDL FUNCTIONS. I have a client who has been given a wsdl file from a company that deals in car testing. My client is a subcontractor for them. They have told us to upload the information about the car plate, category etc and once the details are sent through,There will be a response from server of either success or failure. Kindly assist in this.
Browsing through different information, I tried to do something like below but it is not working
<?php
$data = array('1'=>'value','2'=>'value','3'=>'value','4'='value','5'=>'value');
$wsdl ='http://181.24.80.32/ws/services/servicename';
$client = new SoapClient($wsdl);
$response = $client->servicenamerequest($data);
echo $response->servicenamereturn;
?>
First of all: Go get SoapUI if you are dealing with an unknown Soap service. SoapUI will read the WSDL file and allow you to look at nearly everything related to Soap methods, parameters, and return values (if you dare to make a call to the live service - hopefully there is a sandbox server that does nothing critical).
But SoapUI can help you there by creating a mock service that receives your request and responds with a canned request that you prepared. Here's how I got from your linked WSDL to a working code example without touching the real service.
Setting up SoapUI
Create a new project in SoapUI and give the location of the WSDL. You might name this project.
SoapUI then reads the contents of the WSDL and creates the project containing all described requests. After that you see what methods the service offers, and what kind of parameters have to go into it, as a tree. Opening this tree will get you to "Request 1" for every method detected, which displays some XML in the free version (paid version is slightly more comfortable with a form to fill) like this (from the vehiclePassedTest method):
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:tr="http://tr.gov.tp.stp.ws.PassedVehicleTestService">
<soapenv:Header>
<tr:password>?</tr:password>
<tr:username>?</tr:username>
</soapenv:Header>
<soapenv:Body>
<tr:vehiclePassedTest>
<chassisNo>?</chassisNo>
<plateNo>?</plateNo>
<plateCode>?</plateCode>
<plateCategory>?</plateCategory>
<plcEmiCode>?</plcEmiCode>
<currentUserName>?</currentUserName>
</tr:vehiclePassedTest>
</soapenv:Body>
</soapenv:Envelope>
The questionmarks are where you have to provide data. This request structure has to be created by you using the SoapClient of PHP.
Let's mock this. Mocking means that SoapUI starts it's own Soap server that accepts requests. It usually runs on 127.0.0.1:8080. Right-click on the project "PassedVehicleTestService" and select "New MockService". Accept the name, press Ok.
Right-click on the method you want to mock. Select "Add to mock service" and select the created one from the previous step. Answer yes to open the mock response editor. There you see the XML structure of the answer, with question marks for the data that the service will fill out. All the responses from this service allow one answer as a string, so write something nice that will be returned. You can add the other methods to this service later if needed.
Right-click your "MockService 1" and select "Start minimized". This will start the Soap server, it will wait for an incoming request.
Setup PHP SoapClient for the mock.
You need to know two things. First the location of the WSDL. This file contains the address of the real server to be used for the requests, but PHP allows to override this. So the second info you need is the address of the running mock service.
SoapUI displays the address from the WSDL in the request editor. In your case it is http://181.24.80.32/ws/services/PassedVehicleTestService - the mock service runs on http://127.0.0.1:8080/ws/services/PassedVehicleTestService - IP and port is replaced, the path is kept.
This leads to the first lines of PHP code:
$options = array(
'location' => 'http://127.0.0.1:8080/ws/services/PassedVehicleTestService',
);
$client = new SoapClient("http://www.quickregistration.ae/temp/PassedVehicleTestService.xml", $options);
After that you have a configured SoapClient able to talk to your mock service. If later you want to use the real service, throw the line with "location" out of the array, and keep the other config parameters if you happen to add some.
Talk to the mock
With the client, you can do $client->nameOfSoapMethod(paramStructure). The parameter structure usually is a mixture of arrays or objects and scalar values like strings. So that's what I try first:
$result = $client->vehiclePassedTest(array());
var_dump($result);
Then I look at SoapUI to see what happens. Output from the php script:
Notice: Array to string conversion in [...]/soap.php on line 20
string(1) "?"
SoapUI says:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tr.gov.tp.stp.ws.PassedVehicleTestService">
<SOAP-ENV:Body>
<ns1:vehiclePassedTest>
<chassisNo>Array</chassisNo>
<plateNo/>
<plateCode/>
<plateCategory/>
<plcEmiCode/>
<currentUserName/>
</ns1:vehiclePassedTest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
That was not the right approach. The array got converted into a string "Array", and was placed as the first parameter. The rest is empty. Nice, because I know that soap calls may accept more that one parameter, and this is one of these services.
$result = $client->vehiclePassedTest('chassisNo', 'plateNo', 'plateCode', 'plateCategory', 'plcEmiCode', 'currentUserName');
This shows up correctly in SoapUI, but the headers are still missing.
Adding SoapHeader to the request
The SoapClient offers a method named __setSoapHeaders(), and there is a class SoapHeader.
The description says that setSoapHeaders() accepts one SoapHeader object or an array of SoapHeader objects. Let's create one, pass it and see what happens:
$username = new SoapHeader();
$client->__setSoapHeaders($username);
$result = $client->vehiclePassedTest('chassisNo', 'plateNo', 'plateCode', 'plateCategory', 'plcEmiCode', 'currentUserName');
var_dump($result);
PHP says: Warning: SoapHeader::SoapHeader() expects at least 2 parameters, 0 given
$username = new SoapHeader('namespace', 'username', 'MyUserName');
This works. SoapUI says:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tr.gov.tp.stp.ws.PassedVehicleTestService" xmlns:ns2="namespace">
<SOAP-ENV:Header>
<ns2:username>MyUserName</ns2:username>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<ns1:vehiclePassedTest><chassisNo>chassisNo</chassisNo><plateNo>plateNo</plateNo><plateCode>plateCode</plateCode><plateCategory>plateCategory</plateCategory><plcEmiCode>plcEmiCode</plcEmiCode><currentUserName>currentUserName</currentUserName></ns1:vehiclePassedTest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Note the TWO xmlns attributes in the SOAP-ENV:Envelope element. The original request requires the username be in the namespace "tr" - but "tr" is only a shortcut to the string that is defined as "xlmns:tr" - the string behind this is your namespace needed (although it might already work with the real service).
$username = new SoapHeader("http://tr.gov.tp.stp.ws.PassedVehicleTestService", 'username', 'myUser');
$password = new SoapHeader("http://tr.gov.tp.stp.ws.PassedVehicleTestService", 'password', 'yetAnotherPassword');
$client->__setSoapHeaders(array($username, $password));
This correctly defines the headers, as you can see:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tr.gov.tp.stp.ws.PassedVehicleTestService">
<SOAP-ENV:Header>
<ns1:username>myUser</ns1:username>
<ns1:password>yetAnotherPassword</ns1:password>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<ns1:vehiclePassedTest><chassisNo>chassisNo</chassisNo><plateNo>plateNo</plateNo><plateCode>plateCode</plateCode><plateCategory>plateCategory</plateCategory><plcEmiCode>plcEmiCode</plcEmiCode><currentUserName>currentUserName</currentUserName></ns1:vehiclePassedTest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
No two namespaces in the envelope element, and every element is of namespace "ns1". This should work.
Your final code:
$options = array(
'location' => 'http://127.0.0.1:8080/ws/services/PassedVehicleTestService',
);
$client = new SoapClient("http://www.quickregistration.ae/temp/PassedVehicleTestService.xml", $options);
$username = new SoapHeader("http://tr.gov.tp.stp.ws.PassedVehicleTestService", 'username', 'myUser');
$password = new SoapHeader("http://tr.gov.tp.stp.ws.PassedVehicleTestService", 'password', 'yetAnotherPassword');
$client->__setSoapHeaders(array($username, $password));
$result = $client->vehiclePassedTest('chassisNo', 'plateNo', 'plateCode', 'plateCategory', 'plcEmiCode', 'currentUserName');
var_dump($result);
try something like this. Works for me.
try {
$client = new SoapClient("http://wsdl", array('trace' => 1));
$data = $client->someFunction(array('parma1' => 'value1', 'param2' => 'value2'));
print_r($data);
} catch (SoapFault $fault) {
trigger_error("SOAP Fault: (faultcode: {$fault->faultcode}, faultstring: {$fault->faultstring})", E_USER_ERROR);
exit();
}
And check is you have installed php-soap package in your server.
Greatings.
How can I force my PHP SoapServer to send a JSON object as a response instead of an XML doc?
Thanks.
That is not SOAP, so no. It can incorporate a jsonstring in some xml node, that's about it. You may want just a REST server serving json.
You can bastardize it though, making it by definition NOT SOAP, but some weird hybrid:
<?php
class ThisIsNotASoapServer extends SoapServer {
}
function test(){
//should have a return
//return range(1,9);
//but totally breaks it by:
echo json_encode(range(1,9));
//the exit here is needed
exit;
}
$server = new ThisIsNotASoapServer(null, array('uri' => 'http://test-uri/','soap_version' => 1));
$server->addFunction("test");
$server->handle('<?xml version="1.0"?>
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:Test xmlns:m="Some-URI"/>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>');
?>
... so, technically this is possible, but I suspect there is not a single client which understands this.
Take a look at http://www.sinatrarb.com its very easy to create a restful web service to return a json object.
depends on your requirements - SOAP is a xml request it doesnt mean the format for the return also needs to be SOAP (XML) you can easily post back a JSON string.
Maybe if you can provide more information we can help more?
A question about PHP's SoapClient and SoapServer, WSDL mode.
I need, let's say, create a digest of a certain part of XML with the data in it. With SoapClient it's easy. I overload __doRequest() method from class SoapClient, make hash of certain elements and attach it as the element <Hash></Hash> within <SOAP-ENV:Header/>.
Then I send the resulting XML to the SOAPServer calling parent::__doRequest().
I need to do the same with the response XML. On Server's side I have difficulties. Seems like the Server can only send data as nested arrays or objects, and that somehow is inserted into the response XML on Client's side. I tried sending XML with SoapServer's response, then it returns empty XML.
I really need to parse and modify the XML on Server's side (make hash, digital signature, etc) but so far I haven't found the answer how to do it no matter where I search so I would really really appreciate your help. Thanks.
You will have to get an XML string without XML declaration and parse that with SoapVar.
See how-return-custom-xml-response-in-soapserver-response for details.
In short, it comes to this.
You construct a DOMDocument from your input and any other stuff you need to put into it. XSLT is a nice way to parse and change XML (see above link again for a simple example on how to use xslt in php).
Then you select the DOMNode from the DOMDocument you wish to return, and apply saveXML with this node as parameter. This saves the wanted XML as a string without the declaration. Plain saveXML() without parameter would have saved the root node as a string including the declaration.
Thus:
$nodes = $dom -> getElementsByTagName ('chooseTheElementYouWishToReturn');
$node = $nodes -> item(0);
$result = new SoapVar ($dom -> saveXML($node), XSD_ANYXML);
return ($result);
This also works of course, when you wish to return the root element:
$result = new SoapVar ($dom -> saveXML($dom -> documentElement), XSD_ANYXML);
return ($result);
ADDITION
I noticed that you wish to add something to the SOAP header instead of to the body.
This is a little tricky too but it can be achieved - I hope the below will fit your needs.
First, adding header details can only be done within the function script, and then the function will have to declare the server variable as global in order to refer to the globally (outside the function) declared one, as follows:
<?php
class mySOAPclass {
function xxx ($arg) {
global $server;
// your code for the header part
// example
$auth = array();
$auth['UserName'] = 'user';
$auth['Password'] = 'pw';
$header = new SoapVar ($auth, SOAP_ENC_OBJECT);
// end example
$header = new SoapHeader ('http://schemas.xmlsoap.org/soap/header/', 'credentials', $header, false);
$server -> addSoapHeader ($header);
// your code for the body part, assuming it results in a DOM $dom
$result = new SoapVar ($dom -> saveXML($dom -> documentElement), XSD_ANYXML);
return ($result);
}
ini_set( "soap.wsdl_cache_enabled", "0");
$server = new SoapServer ("yourOwn.wsdl");
$server -> setClass ('mySOAPclass');
$server -> setObject (new mySOAPclass());
$server -> handle();
?>
Note that it is required to first construct a SoapVar from the array; if you feed the array to the header directly you get ugly item/key and item/value nodes.
The above leads to the following return structure:
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns1="http://schemas.xmlsoap.org/soap/header/">
<SOAP-ENV:Header>
<ns1:credentials>
<UserName>user</UserName>
<Password>pw</Password>
</ns1:credentials>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
...
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I'm sending a SOAP request that looks like:
<SOAP-ENV:Body>
<api:GetOrder xsi:type="SOAP-ENC:Struct">
<api_orderId xsi:type="xsd:int">1234</api_orderId>
</api:GetOrder>
</SOAP-ENV:Body>
But needs to look like this (SoapUI generated):
<soapenv:Body>
<api:GetOrder>
<api:orderId>1234</api:orderId>
</api:GetOrder>
</soapenv:Body>
My PHP Code:
$client = $this->getConnection();
$soap_options = array('soapaction' => $config->getValue('soapaction_url') . 'GetOrder');
$obj = new stdClass();
$obj->api_orderId = 59698;
$results = $client->__soapCall('GetOrder', array(new SoapParam($obj, "api:GetOrder")), $soap_options);
2 questions really:
1) How can I remove the "xsi:type" from the request? (If I add xsi:type in to my SoapUI request, I get back a "400 Bad Request"
2) Instead of "api_orderId" I need to send "api:orderId", but I can't name an object with a colon, so do I have to pass the name and value as an array somehow?
Appreciate any help, thank you.
EDIT:
I wasn't able to figure out any other way to send these requests and I essentially ended up doing as Mr.K suggested below.
I wrote a custom class to extend SoapClient. Then overrode the __doRequest method to send my own custom SOAP request.
The only downside is that SOAP no longer returns me an array of objects, so I also had to parse the XML of the SOAP response.
Also I suspect that the performance of doing it this way is a bit slower, but I didn't notice it.
Try with Simple XML parsing, and create the new request as you like.
Read the tag values from Original request, assign those values to a new XML object using parsing. You can create string of XML message and load it as an XML object in PHP.
Just like get it from there, put it inside this..and Send!..