I've spent an evening on this.

Talking to a web service that REQUIRES SOAP 1.2 and some extras.
It complained if I did not send
Content-Type: application/soap+xml;
and even the (allegedly optional) 'action' param in the HTTP headers.

This CAN be done using the options array, but really it's a thing the wsclient service description should take care of.

Changes

  • There is now a variation of the 'soap' endpoint called 'soap 1.2' available in the wsclient service settings. It's actually an alias for the normal wsclient_soap module, but it's a tiny bit more self-aware. The pluggable design of HOOK_wsclient_endpoint_types() was wonderful here!
  • Primarily, it sets the $options['soap_version'] = SOAP_1_2; for you. This means that the PHP lib will send the appropriate headers.
  • Additionally, it adds bonus configuration options to solve #1250914: Setting SOAP headers? !!

    IF you choose 'soap 1.2' - you can now assign additional SOAP envelope Header values. This is available on a per-operation config. If needed, it's likely we should add global ones too, but I've not done that today.

    wsclient-soap_1_2-settings.png

    This produces the required request like so

    POST /Webservice/Boats HTTP/1.1
    Host: example.co.nz:9010
    Connection: Keep-Alive
    User-Agent: PHP-SOAP/5.3.18
    Content-Type: application/soap+xml; charset=utf-8; action="http://example.org.nz/IBoats/CheckSailNumber"
    Content-Length: 534
    
    <?xml version="1.0" encoding="UTF-8"?>
    <env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://example.org.nz" xmlns:ns2="http://www.w3.org/2005/08/addressing">
      <env:Header>
        <ns2:Action env:mustUnderstand="true">http://example.org.nz/IBoats/CheckSailNumber</ns2:Action>
        <ns2:To>http://example.co.nz:9010/Webservice/Boats</ns2:To>
      </env:Header>
      <env:Body>
        <ns1:CheckSailNumber>
          <ns1:BoatType>keelboat</ns1:BoatType>
          <ns1:SailNumber>234</ns1:SailNumber>
        </ns1:CheckSailNumber>
      </env:Body>
    </env:Envelope>
    
  • There should be NO IMPACT on any other methods not using SOAP 1.2, I've been careful about keeping the changes separate.
    With that in mind, I've introduced a tiny bit of copy-paste to replicate exactly the design style that already is in the code. I didn't want to refactor it, so I (slightly) duplicated it.

I found it hard to discover a good reference for exactly what SOAP 1.2 is up to with this stuff, so there was an amount of black-box trial & error, but I finally got what works - for me.

It's hard to give anyone a test plan to try this out. The endpoint I worked against is not just private, it's a black-box and they would not even let us access the WSDL!
Patch forthcoming...

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

dman’s picture

This code should be pretty self-contained. A few lumps of stand-alone modification. (Lets see how drush iqs performs...)

dman’s picture

Looks like I must have missed something - making a new consumer from scratch using soap 1.2 fails to read the WSDL.
Creating it with soap (1) and then changing it to 1.2 does work. I need to look into that.

dman’s picture

Found the extra setting (form validate and submit) where I had to check for both 'soap' and 'soap 1.2' now. Saving as soap 1.2 properly initializes when saving now.

stella’s picture

Very useful feature. However, I don't need to set an actor and so when I attempt to make an api call, I get this warning 3 times:

SoapHeader::SoapHeader(): Invalid actor wsclient_soap.module:85

Happens regardless of whether I set the actor to empty string or NULL, in the code. The attached patch re-roll fixes that by only setting the argument in the SoapHeader call if not empty.

dman’s picture

That makes sense.
I only had one endpoint to test against, I'm not sure how much variation to expect.

imclean’s picture

I couldn't apply the patch.

$ git apply -v 2065037-4-soap_1_2_support.patch
Checking patch wsclient_soap/wsclient_soap.module...
Checking patch wsclient_ui/wsclient_ui.inc...
Checking patch wsclient_ui/wsclient_ui.module...
Checking patch wsclient_tester.inc...
error: wsclient_tester.inc: No such file or directory

However wsclient_tester/wsclient_tester.inc does exist. It doesn't appear to be looking in the subdirectory.

We have a need for nested headers. This is possible with SOAP headers, see this example. The $data parameter is mixed data type.

The UI would be the trickiest part for this I think, but as dman mentioned, we could use the structured-input-field-renderer code from the wsclient_tester.

dman’s picture

#3 applies clean, not sure why #4 has patch problems.

dman’s picture

Here's another attempt at reducing the notice about SoapHeader::SoapHeader(): Invalid actor

(entirely untested, just a visual re-work, but worth a crack)

imclean’s picture

Patch in #8 applies ok but there's no "add more" button for additional headers. To gain an extra row, enter data into the first one the click save and re-edit.

I also tested it for my specific situation to see if it work would without nesting headers, to no avail.

As the headers may be a string or an array or other, the UI could get interesting.

imclean’s picture

The header requirements for each method are actually defined in the WSDL I'm working with. Is this a normal approach?

http://ws.idssasp.com/Members.asmx?wsdl

For e.g. do a search for "GetMemberList" including the quotes.

<wsdl:operation name="GetMemberList">
  <soap:operation soapAction="http://ws.idssasp.com/Members.asmx/GetMemberList" style="document"/>
  <wsdl:input>
    <soap:body use="literal"/>
    <soap:header message="tns:GetMemberListAuthorizeHeader" part="AuthorizeHeader" use="literal"/>
  </wsdl:input>
  <wsdl:output>
    <soap:body use="literal"/>
  </wsdl:output>
</wsdl:operation>

The parameters are also defined, but these don't show up automatically in the method configuration. I suspect there may not be a standard for this.

imclean’s picture

Status: Needs review » Needs work

I just realised the significance of this:

This is available on a per-operation config. If needed, it's likely we should add global ones too, but I've not done that today.

With auth headers and lots of methods this would be very useful.

dman’s picture

> The header requirements for each method are actually defined in the WSDL I'm working with. Is this a normal approach?

I see it, but I've not seen that syntax before. I can't say how normal it is.
All I can say is I don't have an endpoint that does that to test against, so I can't develop for it.

imclean’s picture

Understood. It's not a huge priority anyway. Nested and global headers probably are, and I could potentially take a look at this sometime.

I also think the parameters for each method are being found but not recognised automatically as their Drupal generated machine name is different to their value. Again, not a big issue.

dman’s picture

That thing saying 'parameters' is just a reflection of how wsclient analyses the WSDL. the actual machine name is moot AFAIK, it's the datatype that counts (and works)

dman’s picture

Re above .

The 'add more' button would be a nice UI extra, but at the time when I was mired with actually getting namespaces and protocol right, I skipped that bit and went with the old-school 'here's another if you need it' method.

Seeing as to actually make a full UI work perfectly it looks like we would be adding not just AJAX but NESTED AJAX for complex datatype forms ...I avoided getting lost in that.
It got to the point of 'does what I needed AND is re-usable enough for the next job of the same shape'
The next job of a DIFFERENT shape - I'd not budgeted for.

imclean’s picture

That thing saying 'parameters' is just a reflection of how wsclient analyses the WSDL. the actual machine name is moot AFAIK, it's the datatype that counts (and works)

It does indeed. I was getting confused by the name, which was integer and string where required, and "parameters" otherwise. Also, the WSDL I'm working with involves a single parameter, which is an array of other parameters. So these are all nested too! Much UI fun to be had.

I do understand job priorities. I won't be able to work on this as a patch at the moment but will hopefully have a better understanding and code to share by the end of it.

Semi related: after further reading, I see WSDL is a very well defined and structured language. Pretty much everything can be defined in it so I can see how complete auto-configuration would be possible with the right data in the endpoints.

dman’s picture

Yeah, I was happy with the *ability* for me to automatically build a full form that reflected the WSDL schema required, given enough patience. I had some background with XSD and schema importing before https://drupal.org/sandbox/dman/1126696 , so transferring it to FAPI is actually possible.

Still, making it all AJAXed and 'add more' - is further down the wish-list.

stella’s picture

I have one form on my site for which I need to submit up to 3 SOAP operations, one after the other (I know, ugh!). The SoapHeaders need to be different for each operation, but the __setSoapHeaders() call doesn't overwrite the existing headers unless you set them to NULL first, like this: $client->__setSoapHeaders(NULL);

dman’s picture

Hm. I understand the issue.
Certainly had not anticipated it.
Yeah, I guess resetting the headers to Null before doing anything is a safe way to deal with that. As far as I can imagine.

imclean’s picture

Same as patch #8, against latest git in case anyone wants to play.

Clear your cache after patching.

Horroshow’s picture

Is there something similar for REST header?

hvahid’s picture

Never mind the url was broken.

dman’s picture

Re-roll for fuzz, and one reject due to authentication changes smoothed out

Eric_A’s picture

It just so happens that I rerolled this myself and was going to upload it now. I noticed you made an extra change? Here's the output from a simple diff:

> @@ -113,7 +113,7 @@ function wsclient_tester_prepare_request_callback($form, $form_state) {
>      $args = $form_state['values']['parameters'];
>    }
> 
> -  if ($service->type == 'soap') {
> +  if ($service->type == 'soap' || $service->type == 'soap 1.2') {
>      // The service will have an endpoint that will have a SOAPClient.
>      // Settings on the service->options may be passed to the SOAPClient.
>      // @see WSClientSOAPEndpoint::client()
dman’s picture

Yep, I noticed that the tester was not testing fully as it only handled SOAP and REST, so needed to handle SOAP 1.2 also. Was a minor oversight from earlier patches I guess.
The change mirrors that made in three other places already.

dman’s picture

Re-roll for fuzz

dman’s picture

Status: Needs work » Needs review

I'd like to put this to bed BUT, don't really feel like doing that until we can find a way to do some real testing for regressions and things.

Deciphered’s picture

I know you want to put this to bed, but I have a suggestion, yet not necessarily the time to commit to it myself:

Specific SOAP endpoint sets Authetnication in the Header, and the WSDL provides the an Authentication data structure, but this patch doesn't implement the ability to define a data structure or attach data to the header, plain text only.

I'm not sure how common this practice would be, if it's just something specific to his particular endpoint, but I could see it being useful.

Additionally, I would see it being useful to change the Header values on subsequent Rules, but it appears the header values can only be set via the service definition (and code I assume).

Just my 2c.

Deciphered’s picture

In addition to the last comment, it appears that I can't do what I need to via the UI with the current approach, which is:

    <Authentication xmlns="https://...">
      <UserName>...</UserName>
      <Password>...</Password>
    </Authentication>

I am able to set the header via code though which is a step forward.

Deciphered’s picture

Additionally, while #8 does visually reduce the code for the Actor check, it does re-introduce the error #4 sought to fix, which is regardless of $actor being NULL or an empty string, the error message continues to be set. The argument can not be set at all if it doesn't have a value, and as such there has to be two calls to the function instead of one with a nested conditional.

Re-roll attached.

liquidcms’s picture

Can someone explain the use of additions mentioned in this thread?

I am trying to add the following header to my SOAP request:

  <soap:Header>
    <HTNGHeader xmlns="http://htng.org/1.1/Header/">
      <From>
        <systemId>string</systemId>
        <Credential>
          <userName>string</userName>
          <password>string</password>
        </Credential>
      </From>
      <To>
        <systemId>string</systemId>
      </To>
      <timeStamp>dateTime</timeStamp>
      <echoToken>string</echoToken>
      <transactionId>string</transactionId>
      <action>string</action>
    </HTNGHeader>
  </soap:Header>

is this possible with this patch? Is it possible in code?

even before this patch i could see terms shown in this header listed in the data types under each Operation: http://screencast.com/t/X1sn5X5VH0X0 but have no idea how to utilize this.

Deciphered’s picture

@liquidcms

I had essentially the same scenario, and I can confirm that the patch does not cater for it via the UI. Your header is far more complicated than mine was, which may make it tricky to do via code, but the general concept is this:

function HOOK_wsclient_invoke_arguments_alter(&$arguments, $operation, $service) {
  switch ($service->name) {
    case 'SERVICE_NAME':
      $service->operations[$operation]['header'] = array(
        'HTNGHeader' => array(
          'namespace' => 'http://htng.org/1.1/Header/',
          'name'      => 'From',
          'data'      => array(
            ...
          )
        ),
      );
      break;
  }
}

Although, I might have that slightly wrong. Play with it, and xdebug/debug it further down the line so you can see the xml that is generated.

Ideally the patch needs to be re-worked to allow for structured data to be applied to the header.

liquidcms’s picture

thanks deciphered, dman had shown me how to do this a couple nights ago.. he ended up with this using the same hook you mention:

    $htngheader = array(
      'namespace' => 'http://htng.org/1.1/Header/',
      'name' => 'HTNGHeader',
      'data' => array(
        'From' => array(
          'systemId' => 'systemid value',
          'Credential' => array(
            'userName' => 'username',
            'password' => 'password',
          ),
        ),
        'To' => array(
          'systemId' => 'string',
        ),
        'timeStamp' => date('c'),
        'echoToken' => 'string',
        'transactionId' => 'string',
        'action' => 'our action',
      ),
      'mustunderstand' => '',
    );

    $client->operations[$operation]['header'] = array($htngheader);
TheWrench’s picture

I tried applying the patch in #26 and #30, and both give me a white screen of death. (I'm running the latest Drupal 7, with Web service client 7.x-1.x-dev.)

I really need to create a soap header like this:

<soap:Header>
    <AuthHeader xmlns="http://www.example.com/">
        <username>USERNAME</username>
        <password>PASSWORD</password>
        <tpaid>TPAID</tpaid>
        <AdviceProviderCd>Foo</AdviceProviderCd>
    </AuthHeader>
</soap:Header>

Ultimately what I'm trying to do is retrieve data via SOAP web service and display it on a drupal page (possibly with wsclient-feeds), but I haven't been able to get past this header issue. I do see some people have developed code for a custom module that will add the headers, but then they provide some PHP to "call" it ...thats where I'm getting confused. Would love to just have the same header sent with every request. Any help is greatly appreciated!

Thanks!

dineshw’s picture

Just wondering if that works!
I will be trying it in a next shot!

imclean’s picture

@TheWrench, you'd need to extend WSClientSOAPEndpoint. Add the headers in the call() method. See the code in wsclient I think for an example.

e.g.

/**
 * Overrides WSClientSOAPEndpoint
 *
 * A remote endpoint type for invoking SOAP services.
 */
class WSClientSOAPEndpointWithHeader extends WSClientSOAPEndpoint {
 /**
   * Calls the SOAP service.
   *
   * @param string $operation
   *   The name of the operation to execute.
   * @param array $arguments
   *   Arguments to pass to the service with this operation.
   */
  public function call($operation, $arguments) {
  /* Do your thing here */
  }
}

dineshw’s picture

It works smoothly!

berenddeboer’s picture

Status: Needs review » Reviewed & tested by the community
dman’s picture

Thanks for the bump.
I actually picked this up and added a bunck of tests (OoOOooh!) last weekend - including local stub servers and services to test *against* !
I'll try to rein in my madness there and get something stable-ish to get into a new version number. I was about 3/4 of the way through when I figured I should be putting my examples in the example folder instead of the tests folder, so went to sleep instead of renaming everything I'd been working on.
Will try to re-animate that soon

dman’s picture

Re-roll. Correctly localised to module directory not webroot this time.
This has been useful enough for enough people for long enough, I'll push it in now, so we can address any issues as follow-ups.

  • dman committed 5a7f09d on 7.x-1.x
    Issue #2065037 by dman, Deciphered, imclean, stella, berenddeboer: SOAP...