Completed
Pull Request — master (#11)
by
unknown
01:08
created

SoapClient::___fetchWSDL()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 1
1
<?php
2
/**
3
 * Contains \JamesIArmes\PhpNtlm\NTLMSoapClient.
4
 */
5
6
namespace jamesiarmes\PhpNtlm;
7
8
/**
9
 * Soap Client using Microsoft's NTLM Authentication.
10
 *
11
 * @package php-ntlm\Soap
12
 */
13
class SoapClient extends \SoapClient
14
{
15
    /**
16
     * cURL resource used to make the SOAP request
17
     *
18
     * @var resource
19
     */
20
    protected $ch;
21
22
    /**
23
     * Options passed to the client constructor.
24
     *
25
     * @var array
26
     */
27
    protected $options;
28
29
    /**
30
     * Cache for fetched WSDLs.
31
     * @var array
32
     */
33
    protected static $wsdlCache = [];
34
35
    /**
36
     * {@inheritdoc}
37
     *
38
     * Additional options:
39
     * - user (string): The user to authenticate with.
40
     * - password (string): The password to use when authenticating the user.
41
     * - curlopts (array): Array of options to set on the curl handler when
42
     *   making the request.
43
     * - strip_bad_chars (boolean, default true): Whether or not to strip
44
     *   invalid characters from the XML response. This can lead to content
45
     *   being returned differently than it actually is on the host service, but
46
     *   can also prevent the "looks like we got no XML document" SoapFault when
47
     *   the response includes invalid characters.
48
     * - warn_on_bad_chars (boolean, default false): Trigger a warning if bad
49
     *   characters are stripped. This has no affect unless strip_bad_chars is
50
     *   true.
51
     */
52
    public function __construct($wsdl, array $options = [])
53
    {
54
        // Set missing indexes to their default value.
55
        $options += array(
56
            'user' => null,
57
            'password' => null,
58
            'curlopts' => array(),
59
            'strip_bad_chars' => true,
60
            'warn_on_bad_chars' => false,
61
        );
62
        $this->options = $options;
63
64
        $wsdl = $this->___fetchWSDL($wsdl);
65
66
        // Verify that a user name and password were entered.
67
        if (empty($options['user']) || empty($options['password'])) {
68
            throw new \BadMethodCallException(
69
                'A username and password is required.'
70
            );
71
        }
72
73
        parent::__construct($wsdl, $options);
74
    }
75
76
    /**
77
     * Fetch the WSDL to use.
78
     *
79
     * We need to fetch the WSDL on our own and save it into a file so that the parent class can load it from there.
80
     * This is because the parent class doesn't support overwriting the WSDL fetching code which means we can't add
81
     * the required NTLM handling.
82
     */
83
    protected function ___fetchWSDL($wsdl) {
84
        if (!empty($wsdl) && !file_exists($wsdl)) {
85
            $wsdlHash = md5($wsdl);
86
            if (empty(self::$wsdlCache[$wsdlHash])) {
87
                $temp_file = sys_get_temp_dir() . '/' . $wsdlHash . '.ntlm.wsdl';
88
                if (!file_exists($temp_file) || (isset($this->options['cache_wsdl']) && $this->options['cache_wsdl'] === WSDL_CACHE_NONE)) {
89
                    $wsdl_contents = $this->__doRequest(NULL , $wsdl, NULL, SOAP_1_1);
90
                    // Ensure the WSDL is only stored after validating it roughly.
91
                    if (!curl_errno($this->ch) && strpos($wsdl_contents, '<definitions ') !== FALSE) {
92
                        file_put_contents($temp_file, $wsdl_contents);
93
                    }
94
                    else {
95
                        throw new \SoapFault('Fetching WSDL', sprintf('Unable to fetch a valid WSDL definition from: %s', $wsdl));
96
                    }
97
                }
98
                self::$wsdlCache[$wsdlHash] = $temp_file;
99
            }
100
            $wsdl = self::$wsdlCache[$wsdlHash];
101
        }
102
        return $wsdl;
103
    }
104
105
    /**
106
     * {@inheritdoc}
107
     */
108
    public function __doRequest($request, $location, $action, $version, $one_way = 0)
109
    {
110
        $headers = $this->buildHeaders($action);
111
        $this->__last_request = $request;
112
        $this->__last_request_headers = $headers;
113
114
        // Only reinitialize curl handle if the location is different.
115
        if (!$this->ch
116
            || curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) != $location) {
117
            $this->ch = curl_init($location);
118
        }
119
120
        curl_setopt_array($this->ch, $this->curlOptions($action, $request));
121
        $response = curl_exec($this->ch);
122
123
        // TODO: Add some real error handling.
124
        // If the response if false than there was an error and we should throw
125
        // an exception.
126
        if ($response === false) {
127
            $this->__last_response = $this->__last_response_headers = false;
128
            throw new \RuntimeException(
129
                'Curl error: ' . curl_error($this->ch),
130
                curl_errno($this->ch)
131
            );
132
        }
133
134
        $this->parseResponse($response);
135
        $this->cleanResponse();
136
137
        return $this->__last_response;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function __getLastRequestHeaders()
144
    {
145
        return implode("\n", $this->__last_request_headers) . "\n";
146
    }
147
148
    /**
149
     * Returns the response code from the last request
150
     *
151
     * @return integer
152
     *
153
     * @throws \BadMethodCallException
154
     *   If no cURL resource has been initialized.
155
     */
156
    public function getResponseCode()
157
    {
158
        if (empty($this->ch)) {
159
            throw new \BadMethodCallException('No cURL resource has been '
160
                . 'initialized. This is probably because no request has not '
161
                . 'been made.');
162
        }
163
164
        return curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
165
    }
166
167
    /**
168
     * Builds the headers for the request.
169
     *
170
     * @param string $action
171
     *   The SOAP action to be performed.
172
     */
173
    protected function buildHeaders($action)
174
    {
175
        if (is_null($action)) {
176
            return array(
177
                'Method: GET',
178
                'Connection: Keep-Alive',
179
                'User-Agent: PHP-SOAP-CURL',
180
                'Content-Type: text/xml; charset=utf-8',
181
            );
182
        }
183
        return array(
184
            'Method: POST',
185
            'Connection: Keep-Alive',
186
            'User-Agent: PHP-SOAP-CURL',
187
            'Content-Type: text/xml; charset=utf-8',
188
            "SOAPAction: \"$action\"",
189
            'Expect: 100-continue',
190
        );
191
    }
192
193
    /**
194
     * Cleans the response body by stripping bad characters if instructed to.
195
     */
196
    protected function cleanResponse()
197
    {
198
        // If the option to strip bad characters is not set, then we shouldn't
199
        // do anything here.
200
        if (!$this->options['strip_bad_chars']) {
201
            return;
202
        }
203
204
        // Strip invalid characters from the XML response body.
205
        $count = 0;
206
        $this->__last_response = preg_replace(
207
            '/(?!&#x0?(9|A|D))(&#x[0-1]?[0-9A-F];)/',
208
            ' ',
209
            $this->__last_response,
210
            -1,
211
            $count
212
        );
213
214
        // If the option to warn on bad characters is set, and some characters
215
        // were stripped, then trigger a warning.
216
        if ($this->options['warn_on_bad_chars'] && $count > 0) {
217
            trigger_error(
218
                'Invalid characters were stripped from the XML SOAP response.',
219
                E_USER_WARNING
220
            );
221
        }
222
    }
223
224
    /**
225
     * Builds an array of curl options for the request
226
     *
227
     * @param string $action
228
     *   The SOAP action to be performed.
229
     * @param string $request
230
     *   The XML SOAP request.
231
     * @return array
232
     *   Array of curl options.
233
     */
234
    protected function curlOptions($action, $request)
235
    {
236
        $options = $this->options['curlopts'] + array(
237
            CURLOPT_SSL_VERIFYPEER => true,
238
            CURLOPT_RETURNTRANSFER => true,
239
            CURLOPT_HTTPHEADER => $this->buildHeaders($action),
240
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
241
            CURLOPT_HTTPAUTH => CURLAUTH_BASIC | CURLAUTH_NTLM,
242
            CURLOPT_USERPWD => $this->options['user'] . ':'
243
                               . $this->options['password'],
244
        );
245
246
        // We shouldn't allow these options to be overridden.
247
        $options[CURLOPT_HEADER] = true;
248
        $options[CURLOPT_POST] = true;
249
        $options[CURLOPT_POSTFIELDS] = $request;
250
251
        return $options;
252
    }
253
254
    /**
255
     * Pareses the response from a successful request.
256
     *
257
     * @param string $response
258
     *   The response from the cURL request, including headers and body.
259
     */
260
    public function parseResponse($response)
261
    {
262
        // Parse the response and set the last response and headers.
263
        $info = curl_getinfo($this->ch);
264
        $this->__last_response_headers = substr(
265
            $response,
266
            0,
267
            $info['header_size']
268
        );
269
        $this->__last_response = substr($response, $info['header_size']);
270
    }
271
}
272