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

SoapClient::___fetchWSDL()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 8.8333
c 0
b 0
f 0
cc 7
nc 4
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
                    file_put_contents($temp_file, $wsdl_contents);
91
                }
92
                self::$wsdlCache[$wsdlHash] = $temp_file;
93
            }
94
            $wsdl = self::$wsdlCache[$wsdlHash];
95
        }
96
        return $wsdl;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function __doRequest($request, $location, $action, $version, $one_way = 0)
103
    {
104
        $headers = $this->buildHeaders($action);
105
        $this->__last_request = $request;
106
        $this->__last_request_headers = $headers;
107
108
        // Only reinitialize curl handle if the location is different.
109
        if (!$this->ch
110
            || curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL) != $location) {
111
            $this->ch = curl_init($location);
112
        }
113
114
        curl_setopt_array($this->ch, $this->curlOptions($action, $request));
115
        $response = curl_exec($this->ch);
116
117
        // TODO: Add some real error handling.
118
        // If the response if false than there was an error and we should throw
119
        // an exception.
120
        if ($response === false) {
121
            $this->__last_response = $this->__last_response_headers = false;
122
            throw new \RuntimeException(
123
                'Curl error: ' . curl_error($this->ch),
124
                curl_errno($this->ch)
125
            );
126
        }
127
128
        $this->parseResponse($response);
129
        $this->cleanResponse();
130
131
        return $this->__last_response;
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137
    public function __getLastRequestHeaders()
138
    {
139
        return implode("\n", $this->__last_request_headers) . "\n";
140
    }
141
142
    /**
143
     * Returns the response code from the last request
144
     *
145
     * @return integer
146
     *
147
     * @throws \BadMethodCallException
148
     *   If no cURL resource has been initialized.
149
     */
150
    public function getResponseCode()
151
    {
152
        if (empty($this->ch)) {
153
            throw new \BadMethodCallException('No cURL resource has been '
154
                . 'initialized. This is probably because no request has not '
155
                . 'been made.');
156
        }
157
158
        return curl_getinfo($this->ch, CURLINFO_HTTP_CODE);
159
    }
160
161
    /**
162
     * Builds the headers for the request.
163
     *
164
     * @param string $action
165
     *   The SOAP action to be performed.
166
     */
167
    protected function buildHeaders($action)
168
    {
169
        if (is_null($action)) {
170
            return array(
171
                'Method: GET',
172
                'Connection: Keep-Alive',
173
                'User-Agent: PHP-SOAP-CURL',
174
                'Content-Type: text/xml; charset=utf-8',
175
            );
176
        }
177
        return array(
178
            'Method: POST',
179
            'Connection: Keep-Alive',
180
            'User-Agent: PHP-SOAP-CURL',
181
            'Content-Type: text/xml; charset=utf-8',
182
            "SOAPAction: \"$action\"",
183
            'Expect: 100-continue',
184
        );
185
    }
186
187
    /**
188
     * Cleans the response body by stripping bad characters if instructed to.
189
     */
190
    protected function cleanResponse()
191
    {
192
        // If the option to strip bad characters is not set, then we shouldn't
193
        // do anything here.
194
        if (!$this->options['strip_bad_chars']) {
195
            return;
196
        }
197
198
        // Strip invalid characters from the XML response body.
199
        $count = 0;
200
        $this->__last_response = preg_replace(
201
            '/(?!&#x0?(9|A|D))(&#x[0-1]?[0-9A-F];)/',
202
            ' ',
203
            $this->__last_response,
204
            -1,
205
            $count
206
        );
207
208
        // If the option to warn on bad characters is set, and some characters
209
        // were stripped, then trigger a warning.
210
        if ($this->options['warn_on_bad_chars'] && $count > 0) {
211
            trigger_error(
212
                'Invalid characters were stripped from the XML SOAP response.',
213
                E_USER_WARNING
214
            );
215
        }
216
    }
217
218
    /**
219
     * Builds an array of curl options for the request
220
     *
221
     * @param string $action
222
     *   The SOAP action to be performed.
223
     * @param string $request
224
     *   The XML SOAP request.
225
     * @return array
226
     *   Array of curl options.
227
     */
228
    protected function curlOptions($action, $request)
229
    {
230
        $options = $this->options['curlopts'] + array(
231
            CURLOPT_SSL_VERIFYPEER => true,
232
            CURLOPT_RETURNTRANSFER => true,
233
            CURLOPT_HTTPHEADER => $this->buildHeaders($action),
234
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
235
            CURLOPT_HTTPAUTH => CURLAUTH_BASIC | CURLAUTH_NTLM,
236
            CURLOPT_USERPWD => $this->options['user'] . ':'
237
                               . $this->options['password'],
238
        );
239
240
        // We shouldn't allow these options to be overridden.
241
        $options[CURLOPT_HEADER] = true;
242
        $options[CURLOPT_POST] = true;
243
        $options[CURLOPT_POSTFIELDS] = $request;
244
245
        return $options;
246
    }
247
248
    /**
249
     * Pareses the response from a successful request.
250
     *
251
     * @param string $response
252
     *   The response from the cURL request, including headers and body.
253
     */
254
    public function parseResponse($response)
255
    {
256
        // Parse the response and set the last response and headers.
257
        $info = curl_getinfo($this->ch);
258
        $this->__last_response_headers = substr(
259
            $response,
260
            0,
261
            $info['header_size']
262
        );
263
        $this->__last_response = substr($response, $info['header_size']);
264
    }
265
}
266