PHP SoapClient::__construct uncustomized request - php

I use PHP SoapClient to work with my JAVA application.
Here's a pseudo-code of my extended SoapClient class:
class MyClient extends SoapClient {
public function __construct($url) {
$params = Array (
'login' => 'some_login',
'password' => 'some_password',
'trace' => true,
'exceptions' => true,
'connection_timeout' => 10,
'cache_wsdl' => WSDL_CACHE_NONE,
'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
'encoding' => 'UTF-8'
);
try {
parent::__construct($url, $params);
}
catch(Exception $e) {
/* some logging action */
}
catch(SoapFault $e) {
/* some logging action */
}
}
So I make a new object using my class:
$obj = new MyClient("http://location/file.wsdl");
and it sending a GET request to get xml structure in response:
GET /file.wsdl HTTP/1.0
Host: location
Authorization: Basic ....
A problem is that I can't use SoapClient::__getLastRequest, SoapClient:: __getLastResponse, SoapClient::__getLastResponseHeaders and SoapClient::__setCookie, even SoapClient::_cookies in constructor, because all of these functions are valid after __call or __doRequest only, but a constructor never fire __doRequest.
So I can't customize a constructor request to set and get cookies in result. Because of this my application session management doesn't work correct, because a constructor request fire a new session but I can't get jsessionid cookie in a response.
I've tried to search for ::__construct source code to override it with my own cookie-supported code but nothing.
All of tips in the Internet to use CURL for SOAP requests based on overriding __call or __doRequest functions but a constructor doesnt use them.
Please, any ideas how can I extend a constructor with a custom request?
UPDATE:
I just used soapclient in non-WSDL mode which doesn't fetch a file in a constructor and doesn't send any uncustomized requests. Thanx for your tips, problem solved.

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.

Errors while implementing SOAP Web service with PHP

I have to implement a SOAP Web Service using PHP.
I did it by using the SoapServer class and all works fine.
I need to use a specific format for the request: they have to contain a "Header" tag with an "Authentication" tag in which there is a token that I have to use to authenticate the client that performed the request.
I used "file_get_contents('php //input')" to get the entire request that I received and then parsed it to retrieve the token that I needed.
This works fine if I try to simulate a SOAP request by using SoapUI. But, if I try to do the request by using PHP SoapClient and use the function SoapHeader to set the header, on the server side "file_get_contents('php //input')" returns only the fields of the entire request (contained in the XML tags of the XML request) merged together in a string, instead of returning the entire XML in a string format.
I cannot understand why.
The SoapServer class isn 't well documented in the PHP documentation. The SoapServer class does everything that you have in mind completely automatically. You have to use a decorator class. What a decorator is and what it does I 'll explain in the next lines. I 'm trying to give you a push in the right direction.
A while ago I had to implement the WSSE authentication standard. I 'll take some parts from the WSSE standard for this example.
The incoming request had a header that looked like this ...
<soapenv:Header>
<wsse:Security xmlns:wsc="http://schemas.xmlsoap.org/ws/2005/02/sc" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsc:SecurityContextToken>
<wsc:Identifier>identifier</wsc:Identifier>
</wsc:SecurityContextToken>
</wsse:Security>
</soapenv:Header>
The key (identifier) identifies an authorized user to perform a function of the web service. In this sense, we must check that the key is valid before executing any function. For this purpose we need a decorator class, that is executed before the actual function is executed.
class AuthDecorator
{
/**
* Name of the class, which contains the webservice methods
* #var string
*/
protected $class;
/**
* Flag, if the recieved identifier is valid
* #var boolean
*/
protected $isValid = false;
public function getClass() : string
{
return $this->class;
}
public function setClass($class) : AuthDecorator
{
$this->class = $class;
return $this;
}
public function getIsValid() : bool
{
return $this->isValid;
}
public function setIsValid(bool $isValid) : AuthDecorator
{
$this->isValid = $isValid;
return $this;
}
public function __call(string $method, array $arguments)
{
if (!method_exists($this->class, $method)) {
throw new \SoapFault(
'Server',
sprintf(
'The method %s does not exist.',
$method
)
);
}
if (!$this->getIsValid()) {
// return a status object here, wenn identifier is invalid
}
return call_user_func_array(
[ $this->class, $method ],
$arguments
);
}
/**
* Here 's the magic! Method is called automatically with every recieved request
*
* #param object $security Security node form xml request header
*/
public function Security($security) : void
{
// auth against session or database or whatever here
$identifier = $this->getIdentifierFromSomewhereFunc();
if ($security->SecurityContextToken->Identifier == $identfier) {
$this->setIsValid(true);
}
}
}
That 's the decorator class. Looks easy, hm? The decorator contains a class named like the first child of the xml header of the recieved request. This method will be executed automatically every time we recieve a request with the soap server. Beside that the decorator checks, if the called soap server function is available. If not a soap fault is thrown that the soap client on the consumer side recieves. If a method exists is quite easy, too. Every webservice method we put in a class.
class SimpleWebservice
{
public function doSomeCoolStuff($withCoolParams) : \SoapVar
{
// do some fancy stuff here and return a SoapVar object as response
}
}
For illustrative purposes, our web service just has this one function.
But how the hell we bring the decorator to work with the soap server?
Easy, mate. The SoapServer class has some pretty tricky functionality, that is not documented. The class has a method called setObject. This method will do the trick.
$server = new \SoapServer(
$path_to_wsdl_file,
[
'encoding' => 'UTF-8',
'send_errors' => true,
'soap_version' => SOAP_1_2,
]
);
$decorator = new AuthDecorator();
$decorator->setClass(SimpleWebservice::class);
$server->setObject($decorator);
$server->handle();
That 's awesome, right? Just initializing the SoapServer class, add the decorator with the setObject method and run it with the handle method. The soap server recieves all requests and before calling the webservice method the decorator will check, if the identifier is valid. Only if the identifier is valid, the called webservice method will be executed.
How 's the soap client request looking?
On the other side the soap client can look like this ...
$client = new SoapClient(
$path_to_wsdl_file,
[
'cache_wsdl' => WSDL_CACHE_NONE,
'compression' => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP,
'exceptions' => true,
'trace' => true,
]
);
$securityContextToken = new \stdClass();
$securityContextToken->Identifier = 'identifier';
$securityContextToken = new \SoapVar(
$securityContextToken,
SOAP_ENC_OBJ,
null,
null,
'SecurityContextToken',
'http://schemas.xmlsoap.org/ws/2005/02/sc'
);
$security = new stdClass();
$security->SecurityContextToken = $securityContextToken;
$security = new \SoapVar(
$security,
SOAP_ENC_OBJ,
null,
null,
'Security',
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
);
$header = new \SoapHeader(
'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
'Security',
$security
);
$client->__setSoapHeaders($header);
$result = $client->doSomeCoolStuff(new \SoapParam(...));
Conclusion
When working in an object orientated context the SoapServer and SoapClient classes are pretty cool. Because the documentation doesn 't really give much about both classes, you have to test and learn. You can easily create a SOAP webservice when you know how. Without writing any xml as a string.
Before you productively use the code examples seen here, please make sure that they are only examples and not intended for productive use. The shown examples should push you in the right direction. ;)
Questions?

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.

SOAP Server in TYPO3 6.2 extension

I'm using TYPO3 6.2 and I want to implement a SOAP Server within my existing extbase extension. Later on I want to be able to push data through a SOAP request which is then saved to a database.
The extensinon key of my plugin is soap_parking_deck and the vendor is Comkom. In my extension I have a class Classes/Service/SOAPService.php :
namespace Comkom\SoapParkingDeck\Service;
class SOAPService {
public function __construct() {
try {
$server = new SOAPServer (
NULL,
array (
'uri' => 'http://localhost/test/SOAPService',
'encoding' => 'UTF-8',
'soap_version' => SOAP_1_2
)
);
$server->addFunction('helloWorld');
$server->handle();
}
catch (SOAPFault $fault) {
print $fault->faultstring;
}
}
public function helloWorld() {
return 'Hello World';
}
}
Within the class I'm defining a PHP SOAPServer and a function helloWorld(). But when I try to make a request I get a 404 error.
With the hint of Arek van Schaijk I figured out a solution.
The 404-Error occured because the uri actually has to be the full path to the Server file.
namespace Comkom\SoapParkingDeck\Service;
class SOAPService {
public function helloWorld() {
return 'Hello World';
}
}
try {
$server = new \SOAPServer (
NULL,
array (
'uri' => 'http://localhost/test/typo3conf/ext/soap_parking_deck/Classes/Service/SOAPService',
'encoding' => 'UTF-8',
'soap_version' => SOAP_1_1
)
);
$server->setClass('Comkom\SoapParkingDeck\Service\SOAPService');
$server->handle();
}
catch (\SOAPFault $fault) {
print $fault->faultstring;
}
Your code example can't work since you're trying to call the class \Comkom\SoapParkingDeck\Service\SOAPServer instead of \SoapServer and \Comkom\SoapParkingDeck\Service\SOAPFault instead of \SoapFault.
In a namespaced environment you should declare the \ (backward slash) for calling php- classes (see also How to use “root” namespace of php?).
Debugging
While development you should enable debug output, deprecation logs and set logging to info level. This can be easily done in the install tool under Configuration presets => Development / Production settings => Development.
Please check also always your apache errorlog if you deal with rare behavior like white screens, 500 internal server errors etc.

cakephp & nusoap - Function ("my_function") is not a valid method for this service

I have taken over an application from a previous developer, and there was some code which was half finished using Nusoap, I am getting the error:
Call to register_donation() web service failed due to an exception:
Function ("register_donation") is not a valid method for this service
The application is built on: CakePHP 1.2.10 & using nuSoap 0.9.5.
I have already ini_set('soap.wsdl_cache_enabled', 0); (This doesnt help.)
My code is below (I shortened it for readability), Am I processing this response correctly?
(The code was taken over by myself from a previous developer).
The Pastebin (of the full code) is here: PasteBin Link
A shortened version is below for a glance over, the full version is in the pastebin link above.
<?php
ini_set('soap.wsdl_cache_enabled', 0);
class ServicesController extends AppController
{
var $uses = array("Donation");
var $components = array( "Email", "MatchingEvents" );
function registerDonation($donation = null){
$this->log('hit registerDonation', 'donation');
$this->autoRender = false;
App::import('Vendor','nusoap');
Configure::write('debug',0);
Configure::write('Session.start', false);
//init soap server
$server = new soap_server();
$endpoint = 'http://new.mysite.com/services/registerDonation';
//initialize WSDL support
$server->configureWSDL('donations', 'urn:donations', $endpoint);
//setup service type
$server->wsdl->addComplexType(
'DonationResult',
'complexType',
'struct',
'all',
'',
array(
'success' => array('name' => 'success', 'type' => 'xsd:boolean'),
'msg' => array('name' => 'msg', 'type' => 'xsd:string'),
'error_number' => array('name' => 'error_number', 'type' => 'xsd:string')
)
);
//register the method to expose
$server->register('register_donation',
array('ct_number' => 'xsd:string', 'project_id' => 'xsd:int', 'donation_amount' => 'xsd:decimal',
// Stripped all other params
),
array(
'result' => 'tns:DonationResult'
),
'urn:donations',
'urn:donations#register_donation',
'rpc',
'encoded',
'Accepts the results of a donation to a charity or project on the site'
);
//This inner function is registered and then called (keep within outer function!)
function register_donation(
// Pass in all the params (Stripped for readability)
$ct_number = null, $project_id = null, $donation_amount = null
){
// This function is never hit!.. its not registered, why?
$this->log('hit #3-register_donation called!', 'donation');
$return = $this->Donation->add(array(
'unique_id' => $unique_id,
'ct_number' => $ct_number,
'project_id' => $project_id,
'donation_amount' => $donation_amount,
// Pass in all other params, (Stripped for readability)
));
// Process that request
$log = $this->Donation->log . 'Result: ' . $return['msg'] . "\n";
$this->log( $log, 'donation' );
if ( isset($this->Donation->matching['events']) )
{
//Reserved donations should already have had their events handled
$this->MatchingEvents->handle($this->Donation->matching, !$this->Donation->matching['reserved']);
}
if ( !empty($this->Donation->cacheSaved) )
$this->_sendDonationEmails($this->Donation->cacheSaved);
return $return;
}
$HTTP_RAW_POST_DATA = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : '';
$server->service($HTTP_RAW_POST_DATA);
}
function _sendDonationEmails($donation)
{
// Send the emails
}
}
?>
If there is any more information I can provide, please let me know.
To Summarise: How do I process a nusoap response coming from a external source.
Any debugging ideas, hints & tips or solution will be rewarded (+1!).
In the code posted there's a function named registerDonation inside a controller named ServicesController. Inside that function there is another function called register_donation (This obviously doesn't make sense [Old Code]).
Remove the function named register_donation out of the registerDonation method and place it as a method inside the ServicesContoller.
Then change the following line (where you register the function to expose).
From: $server->register('register_donation',
To: $server->register('ServicesController.register_donation',
So you're calling class.method as opposed to method only (This would work in PHP4 procedural programming, but in OOP, you need to specify the controller.method when exposing the function).
Having other problems?
Q: CakePHP & nuSOAP is not hitting my controller/method, but redirects to app/webroot
A: Using CakePHP 1.2, I found when it did not have a services model (because it wasn't
technically required), The request does not hit the controller. So if you're having this issue, create a model for your controller, even if you're not using it. For your reference, here a modal example:
<?php // You may not need this modal, but cakephp requires it to be there.
class Services extends AppModel {
public $name = 'Services';
public $useTable = false;
}
?>
Q:What URL should I be receiving responses to?
A: The URL you set your response to hit must include ?wsdl.
So.. for eg: you have a controller named SOAPController and method named process.
In CakePHP, your URL would look like this: mydomain.com/SOAP/process.
So your WSDL is located at mydomain.com/SOAP/process?wsdl. Make sure your callback URL is set to this (including the wsdl).
Q: How do I debug SOAP requests/responses using CakePHP?
A: CakePHP has a logging feature which proved invaluable in debugging SOAP. In your controller (or modal) you can use:
$this->log('your message or variable', 'name_of_file_to_save_to);
You can use this through your SOAP request or response to see what parts are being hit/called and debugging variables (eg: $HTTP_RAW_POST_DATA).
Q: My WSDL location is shown as `domain.com/app/webroot` when I visit the SOAP page.
A: I thought this was the issue causing all the problems, Looking at the source code Nusoap uses PHP_SELF to get the current script (which in cakePHP is app/webroot/index.php), Don't worry about this, you can access the wsdl by appending the URL with ?wsdl, This will show you your generated WSDL file, You don't need to worry about fixing this. Its not interfering with your SOAP request whatsoever, It's merely there for your convenience.
Q: My SOAP address location is showing 'domain.com/app/webroot/index.php`
A: This was a issue I fixed by including the $endpoint in the configureWSDL().
//Set our endpoint, Replace with your URL
$endpoint = 'http://yourdomain.com/controller/method';
//initialize WSDL support - Include the $endpoint.
$server->configureWSDL('donations', 'urn:donations', $endpoint);
// Now your Soap Address Location should be what you set as the $endpoint

Categories