SoapServer behind Symfony 3 controller not calling wrapped method - php

Overview
I'm trying to put a SOAP server inside a Symfony controller as described here: http://symfony.com/doc/current/controller/soap_web_service.html.
I'm using PHP2WSDL to generate the WSDL (after having failed to create a valid one by hand!). I have a service class with a bunch of methods that will be exposed via SOAP. And the Symfony controller will shovel the POST data into the SoapServer instance and barf out the yukky XML response back to the client.
But I'm pretty sure that SoapServer is not calling the methods on my service class. I can see from SoapServer::getFunctions that all the PHP2WSDL #soap annotated methods have been registered by my SoapServer instance, but nothing my methods are supposed to do seems to be happening; no logging, not even throw new \Exception('##!?!'); on the first line.
The actual symptom I have is that, after the SoapClient tries to __soapCall a method (in the other application), it expects to have received some SoapHeaders (which my method might be setting; I just don't know) but the response headers are empty.
Code
Commented lines are things I've been using to try and debug.
Symfony controller
class DefaultController extends Controller
{
const SERVICE_URI = 'http://example.com/api';
public function wsdlAction()
{
$log = new Logger('api');
$response = new Response();
$response->headers->set('Content-Type', 'text/xml');
$response->setContent($this->makeWsdl());
// $log->debug(sprintf('[%s] WSDL response', __METHOD__));
return $response;
}
public function operationAction(Request $request)
{
$log = new Logger('api');
try {
$wsdlFile = tmpfile();
$wsdlPath = stream_get_meta_data($wsdlFile)['uri'];
file_put_contents($wsdlPath, $this->makeWsdl());
} catch (Exception $e) {
$log->error(sprintf('[%s] Couldn\'t create the temporary WSDL file for SoapServer: "%s"', __METHOD__, $e->getMessage()));
throw $e;
}
// $c = file_get_contents($wsdlPath);
// $log->debug(sprintf('[%s] Created WSDL temporary file in %s: %s', __METHOD__, $wsdlPath, $c));
$server = new SoapServer(
$wsdlPath,
[
'soap_version' => SOAP_1_1,
'encoding' => 'UTF-8',
]
);
// $log->debug(sprintf('[%s] Created server', __METHOD__));
$server->setObject(new MyService($server, $log));
// $log->debug(sprintf('[%s] Set server object', __METHOD__));
// foreach ($server->getFunctions() as $f) {
// $log->debug(sprintf('[%s] SOAP function: %s', __METHOD__, (string) $f));
// }
$response = new Response();
$response->headers->set('Content-Type', 'text/xml; charset=UTF-8');
// $log->debug(sprintf('[%s] Create new response', __METHOD__));
ob_start();
// $log->debug(sprintf('[%s] Calling server->handle(%s)', __METHOD__, $request->getContent()));
$server->handle($request->getContent());
// $log->debug(sprintf('[%s] Setting response content', __METHOD__));
$response->setContent(ob_get_clean());
// $log->debug(sprintf('[%s] operate response', __METHOD__));
return $response;
}
private function makeWsdl()
{
$wsdlGenerator = new PHP2WSDL\PHPClass2WSDL(MyService::class, self::SERVICE_URI);
$wsdlGenerator->generateWSDL(true);
return $wsdlGenerator->dump();
}
}
This controller mostly "works". I mean, you can call the route for the WSDL and you get the WSDL. And you can call the route for the operations and it does all the work up to $server->handle($request->getContent()); where it throws an exception.
MyService
class MyService
{
private $server;
private $log;
public function __construct(SoapServer $server, Logger $log)
{
$this->server = $server;
$this->log = $log;
// $this->log->debug(sprintf('[%s] __construct called', __METHOD__));
}
/**
* #soap
*/
public function Login(array $parameters = [])
{
// throw new \Exception('Login throws immediately');
// $this->log->debug(sprintf('[%s] Login called: %s', __METHOD__, json_encode($parameters)));
$this->server->addSoapHeader(new SoapHeader(
'http://example.com/api',
'AuthHeader',
['Token' => 'fake-token']
));
return true;
}
}
The WSDL is generated from this class by PHP2WSDL which seems to be working perfectly.
The external application that calls my controller is throwing an exception because it's expecting to get this AuthHeader SOAP header in the SOAP response that comes from my DefaultController. But it isn't finding one. But whether or not my attempt to use addSoapHeader here is correct, I can't see that this Login method is even being called.
The external application
$this->client = new SoapClient('http://example.com/api/wsdl', [
'trace' => 1,
'encoding' => 'UTF-8',
'soap_version' => SOAP_1_1,
'cache_wsdl' => WSDL_CACHE_NONE,
'location' => 'http://example.com/api',
]);
...
$response = $this->client->__soapCall(
'Login',
['username' => '', 'password' => ''],
[],
null,
$outputHeaders
);
...
if ($outputHeaders['AuthHeader']) {
...
This is the code that ultimately throws the exception when $outputHeaders['AuthHeader'] is not set. From here, I can dump the SOAP requests and responses. Here's the request:
<?xml version="1.0" encoding="UTF-8"?>
<envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://example.com/api/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns2="http://xml.apache.org/xml-soap" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" soap-env:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/">
<body>
<login>
<parameters xsi:type="ns2:Map">
<item>
<key xsi:type="xsd:string">username</key>
<value xsi:type="xsd:string"></value>
</item>
<item>
<key xsi:type="xsd:string">password</key>
<value xsi:type="xsd:string"></value>
</item>
</parameters>
</login>
</body>
</envelope>
And the response, of course, is empty; as are the output headers.
This code is the "legacy" bit that I can't change. For a bit more background: it calls a real, third-party SOAP service and what I'm trying to do at the moment is build a mock of that service that can be used for testing.

Related

Build correct SOAP Request Header (PHP)

I have to do requets to a SOAP API with PHP and I need the following SOAP-Header structure:
<soapenv:Header>
<ver:authentication>
<pw>xxx</pw>
<user>xxx</user>
</ver:authentication>
</soapenv:Header>
How can I build this header?
I tried
$auth = [
"ver:authentication" => [
"pw" => $this->pw,
"user" => $this->user
]
];
$options = [];
$options["trace"] = TRUE;
$options["cache_wsdl"] = WSDL_CACHE_NONE;
$options["compression"] = SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP;
$client = new SoapClient("www.my-url.com/wsdl", $options);
$header = new SoapHeader("www.my-url.com", "authentication", $auth, false);
$client->__setSoapHeaders($header);
but it does not work. The respons is "failure" which I get, when the header structure is incorrect...
please help
the solution could be object driven. In the following code an example is given. Please keep in mind, that the following code is not testet.
class Authentication
{
protected $user;
protected $pw;
public function getUser() : ?string
{
return $this->user;
}
public function setUser(string $user) : Authentication
{
$this->user = $user;
return $this;
}
public function getPw() : string
{
return $this->pw;
}
public function setPw(string $pw) : Authentication
{
$this->pw = $pw;
return $this;
}
}
The above shown class is a simple entity, which contains two properties $user fpr the username and $pw for the password. Further it contains the getter and setter functions for retrieving or setting the values for the two properties.
For the next step just fill the class with data and store it in a SoapVar object.
$authentication = (new Authentication())
->setUser('Username')
->setPw('YourEncodedPassword');
$soapEncodedObject = new \SoapVar(
$authentication,
SOAP_ENC_OBJECT,
null,
null,
'authentication',
'http://www.example.com/namespace'
);
As you can see above, your authentication class will be stored as soap var object. It is encoded as soap object. The only thing you have to do is setting the namespace for this object. In your given example it is ver:. With this namespace prefix somewhere in your wsdl file a namespace is noted. You have to find out this namespace url and just replace the example url http://www.example.com/namespace with the right url noted in your wsdl.
The next step is setting this as soap header. That 's quite simple.
try {
$client = new SoapClient('http://www.example.com/?wsdl', [
'trace' => true,
'exception' => true,
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
]);
// set the soap header
$header = new SoapHeader('http://www.example.com/namespace', 'authentication', $authentication, false);
$client->setSoapHeaders($header);
// send the request
$result = $client->someWsdlFunction($params);
} catch (SoapFault $e) {
echo "<pre>";
var_dump($e);
echo "</pre>";
if ($client) {
echo "<pre>";
var_dump($client->__getLastRequest());
echo "</pre>";
echo "<pre>";
var_dump($client->__getLastResponse());
echo "</pre>";
}
}
As you can see it 's a bit different from your given example. Instead of an array it 's the soap encoded authentication object, that is given to the soap header class. For failure purposes there is a try/catch block around your soap client. In that case you can identify the error and if the client was initiated correctly, you can also see the last request and last response in xml.
I hope, that I helped you. ;)
I would strongly advise you 2 things:
Use a WSDL to PHP generator in order to properly construct your request. In addition, it will ease you the response handling. Everything is then using the OOP which is much better. Take a look to the PackageGenerator project.
Use the WsSecurity project in order to easily add your dedicated SoapHeader without wondering how to construct it neither.

WSSE Security PHP SoapServer- Header not understood

I have a client call with a WSSE Security Header:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header><wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><wsse:UsernameToken wsu:Id="UsernameToken-7BCCD9337425FBA038149772606059420"><wsse:Username>USERNAME</wsse:Username><wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">PASSWORD</wsse:Password><wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">NONCE</wsse:Nonce><wsu:Created>2017-06-17T19:01:00.594Z</wsu:Created></wsse:UsernameToken></wsse:Security></soapenv:Header>
<soapenv:Body>
<ns:FUNCTION/>
</soapenv:Body>
</soapenv:Envelope>
As SoapServer I have a simple one:
// the SOAP Server options
$options=array(
'trace' => true
, 'cache_wsdl' => 0
, 'soap_version' => SOAP_1_1
, 'encoding' => 'UTF-8'
);
$wsdl = 'http://localhost/index.php?wsdl';
$server = new \SoapServer($wsdl, $options);
$server->setClass('ServerClass');
$server->handle();
$response = ob_get_clean();
echo $response;
As soon the property MustUnderstand = 1, then I become from the server the exception: Header not understood.
How to understand the header?
How to make the WSSE validation on the SoapServer side?
The solution is very tricky! I don't know why is this handled by this way from the SoapServer, but here is the solution:
class ServerClass {
public function Security($data) {
// ... do nothing
}
public function myFunction(){
// here the body function implementation
}
}
We need to define a function in our class, which is handling the soap request with the name of the header tag, which is holding the soap:mustUnderstand property. The function doesn't need to be implemented in some way.
That's all!
Mutatos' question / answer got me on the right track. I was working outside of a class structure so what worked for me was the following:
function Security($data)
{
$username = $data->UsernameToken->Username;
$password = $data->UsernameToken->Password;
//check security credentials here
}
$server = new SoapServer("schema/wsdls/FCI_BookingPullService.wsdl", array('soap_version' => SOAP_1_2));
$server->addFunction("Security");
$server->handle();
Essentially a function with the same name as the SOAP header "<wsse:Security>" (ignore the namespace) is being defined, then telling the server to use that to process the header with the 'addFunction' method.
Not ideal from a scope point of view, if that's an issue, try the class approach.

How to change the response function name in Zend Soap Server?

I'm generating a WSDL and I need to change the function name in the response to match the name from the client (which I have no control over).
Here's the WSDL response I'm getting:
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://localhost:8000/soap/index.php?wsdl" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:fooFunctionResponse>
<return xsi:type="xsd:boolean">true</return>
</ns1:fooFunctionResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I need the line <ns1:fooFunctionResponse> to read <ns1:fooFunctionAcknowledgement>.
Here is my Soap server:
if (isset($_GET['wsdl'])) {
ini_set('soap.wsdl_cache_enabled', 0);
$soapAutoDiscover = new \Zend\Soap\AutoDiscover(new \Zend\Soap\Wsdl\ComplexTypeStrategy\ArrayOfTypeSequence());
$soapAutoDiscover->setBindingStyle(array('style' => 'document'));
$soapAutoDiscover->setOperationBodyStyle(array('use' => 'literal'));
$soapAutoDiscover->setClass('SoapFunction');
$soapAutoDiscover->setUri(http://localhost:8000/soap/index.php);
$soapAutoDiscover->handle();
} else {
$soap = new \Zend\Soap\Server(null, array("soap_version" => SOAP_1_2, 'uri' => 'http://localhost:8000/soap/index.php?wsdl', 'classmap' => array('Identification', 'RemoteIdentification')));
$soap->setClass('SoapFunction');
$soap->handle();
}
I've found this line of code in the Zend framework (Autodiscover.php line 514) which looks like it controls the naming of the function:
$element = [
'name' => $functionName . 'Response',
'sequence' => $sequence
];
But changing it does nothing at all, the parent method is never called. I've no idea how to solve this problem, please help.
I've discovered that this line does change the function name, however I'm using SoapUI to test my API, and from SoapUI I always see Response instead of whatever I change the string to. In Chrome, I see Acknowledgement.
Why does SoapUI show a different function name?
I don't know much about Zend Framework and/or it's structure. But I think you can simply extend the class containing the response() method and overwrite the method.
// Example Zend Class
class Zend_Class {
public function response()
{
// ...
}
}
// Your own Class
class Own_Class extends Zend_Class
{
public function ResponseBoohoo()
{
return parent::response();
}
}
If I totally and utterly misunderstood you, I apologize. :)

SoapFault - class not found after response

I get this SoapFault I dont understand. Calling the function below, codewordStemExists(), should create a SoapClient which connects to a SoapServer that is up and running (no errors that I can found has been reported from the server side).
private static function initClient() {
ini_set("soap.wsdl_cache_enabled", "0");
$classmap = array(
'CodewordStemExists' => 'CodewordStemExists',
'CodewordStemExistsResponse' => 'CodewordStemExistsResponse',
);
$client = new \SoapClient("http://..../service.wsdl", array(
"trace" => true,
"exceptions" => true,
"classmap" => $classmap
));
return $client;
}
public static function codewordStemExists($stem) {
$client = self::initClient();
try {
$req = new CodewordStemExists();
$req->username = "....";
$req->password = "....";
$req->codewordStem = $stem;
$res = $client->codewordStemExists($req);
return (bool)$res->result;
}
catch (\SoapFault $e) {
var_dump($client->__getLastResponse());
}
/** The result from var_dump: */
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://....">
<SOAP-ENV:Body>
<ns1:CodewordStemExistsResponse><ns1:result>false</ns1:result>
</ns1:CodewordStemExistsResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The SoapFault:
Class 'CodewordStemExistsResponse' not found
CodewordStemExistsResponse is required at bootstrapping, it is possible to instantiate it at any time.
Anyone seen this before? Thanks.
Check if this needs proper namespacing, e.g. \vendor\CodewordStemExistsResponse.

Getting parameter names from SOAP request with PHP SOAP extension?

Given that the following client.php creates this request XML:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://soap.dev/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:Test>
<RequestId xsi:type="xsd:int">1</RequestId>
<PartnerId xsi:type="xsd:int">99</PartnerId>
</ns1:Test>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
How do I access the names of the parameters? (RequestId and PartnerId) inside a server.php? The names are clearly there in the payload, but on server side only the values are received (1 and 99)
Sample code follows:
client.php
<?php
$client_params = array(
'location' => 'http://soap.dev/server.php',
'uri' => 'http://soap.dev/',
'trace' => 1
);
$client = new SoapClient(null, $client_params);
try {
$res = $client->Test(
new SoapParam(1, 'RequestId'),
new SoapParam(99, 'PartnerId')
);
} catch (Exception $Ex) {
print $Ex->getMessage();
}
var_dump($client->__getLastRequest());
var_dump($client->__getLastResponse());
server.php
class receiver {
public function __call ($name, $params)
{
$args = func_get_args();
// here $params equals to array(1, 99)
// I want the names as well.
return var_export($args, 1);
}
}
$server_options = array('uri' => 'http://soap.dev/');
$server = new SoapServer(null, $server_options);
$server->setClass('receiver');
$server->handle();
Please note that I can not really change the incoming request format.
Also, I am aware that I could give the names back to parameters by creating a Test function with $RequestId and $PartnerId parameters.
But what I really want to is to get name/value pairs out of incoming request.
So far the only idea I have is to simply parse the XML and this cannot be right.
Back when I had this problem I finally decided to go with the proxy function idea - Test function with parameters named ($RequestId and $PartnerId) to give names back to parameters. Its adequate solution, but certainly not the best.
I have not yet lost the hope for finding a better solution though, and here is my best idea so far
<?php
class Receiver {
private $data;
public function Test ()
{
return var_export($this->data, 1);
}
public function int ($xml)
{
// receives the following string
// <PartnerId xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xsd:int">99</PartnerId>
$element = simplexml_load_string($xml);
$this->data[$element->getName()] = (string)$element;
}
}
$Receiver = new Receiver();
$int = array(
'type_name' => 'int'
, 'type_ns' => 'http://www.w3.org/2001/XMLSchema'
, 'from_xml' => array($Receiver, 'int')
);
$server_options = array('uri' => 'http://www.w3.org/2001/XMLSchema', 'typemap' => array($int), 'actor' => 'http://www.w3.org/2001/XMLSchema');
$server = new SoapServer(null, $server_options);
$server->setObject($Receiver);
$server->handle();
It still means parsing XML manually, but one element at a time which is a bit more sane that parsing entire incoming SOAP message.

Categories