Completed
Push — master ( 53001c...a108e7 )
by Gareth
03:19
created

EWSAutodiscover::getServerFromResponse()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 9.2
cc 4
eloc 5
nc 3
nop 1
crap 20
1
<?php
2
namespace garethp\ews\API;
3
4
use garethp\ews\API;
5
use garethp\ews\API\Exception\AutoDiscoverFailed;
6
use garethp\ews\HttpPlayback\HttpPlayback;
7
8
class EWSAutodiscover
9
{
10
    const AUTODISCOVER_PATH = '/autodiscover/autodiscover.xml';
11
12
    /**
13
     * @var HttpPlayback
14
     */
15
    protected $httpPlayback;
16
17
    protected function __construct()
18
    {
19
    }
20
21
    /**
22
     * Parse the hex ServerVersion value and return a valid
23
     * ExchangeWebServices::VERSION_* constant.
24
     *
25
     * @param $versionHex
26
     * @return string|boolean A known version constant, or FALSE if it could not
27
     * be determined.
28
     *
29
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
30
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
31
     */
32 4
    public function parseServerVersion($versionHex)
33
    {
34
        //Convert from hex to binary
35 4
        $versionBinary = base_convert($versionHex, 16, 2);
36 4
        $versionBinary = str_pad($versionBinary, 32, "0", STR_PAD_LEFT);
37
38
        //Get the relevant parts of the binary and convert them to base 10
39 4
        $majorVersion = base_convert(substr($versionBinary, 4, 6), 2, 10);
40 4
        $minorVersion = base_convert(substr($versionBinary, 10, 6), 2, 10);
41
42
        $versions = [
43
            8 => [
44 4
                'name' => 'VERSION_2007',
45
                'spCount' => 3
46 4
            ],
47
            14 => [
48 4
                'name' => 'VERSION_2010',
49
                'spCount' => 3
50 4
            ],
51
            15 => [
52 4
                'name' => 'VERSION_2013',
53
                'spCount' => 1
54 4
            ]
55 4
        ];
56
57 4
        if (!isset($versions[$majorVersion])) {
58 1
            return false;
59
        }
60
61 3
        $constant = $versions[$majorVersion]['name'];
62 3
        if ($minorVersion > 0 && $minorVersion <= $versions[$majorVersion]['spCount']) {
63 1
            $constant .= "_SP$minorVersion";
64 1
        }
65
66 3
        return constant(ExchangeWebServices::class . "::$constant");
67
    }
68
69
    protected function newAPI($email, $password, $username = null, $options = [])
70
    {
71
        $options = array_replace_recursive([
72
            'httpPlayback' => [
73
                'mode' => null
74
            ]
75
        ], $options);
76
77
        $this->httpPlayback = HttpPlayback::getInstance($options['httpPlayback']);
78
79
        if (!$username) {
80
            $username = $email;
81
        }
82
83
        $settings = $this->discover($email, $password, $username);
84
        if (!$settings) {
85
            throw new AutoDiscoverFailed();
86
        }
87
88
        $server = $this->getServerFromResponse($settings);
89
        $version = $this->getServerVersionFromResponse($settings);
90
91
        if (!$server) {
92
            throw new AutoDiscoverFailed();
93
        }
94
95
        $options = [];
96
        if ($version) {
97
            $options['version'] = $version;
98
        }
99
100
        return API::withUsernameAndPassword($server, $email, $password, $options);
101
    }
102
103
    protected function getServerVersionFromResponse($response)
104
    {
105
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
106
        foreach ($response['Account']['Protocol'] as $protocol) {
107
            if (($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
108
                && isset($protocol['ServerVersion'])
109
            ) {
110
                return $this->parseServerVersion($protocol['ServerVersion']);
111
            }
112
        }
113
114
        return false;
115
    }
116
117
    public function getServerFromResponse($response)
118
    {
119
        foreach ($response['Account']['Protocol'] as $protocol) {
120
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
121
                return $protocol['Server'];
122
            }
123
        }
124
125
        return false;
126
    }
127
128
    /**
129
     * Static method may fail if there are issues surrounding SSL certificates.
130
     * In such cases, set up the object as needed, and then call newEWS().
131
     *
132
     * @param string $email
133
     * @param string $password
134
     * @param string $username If left blank, the email provided will be used.
135
     * @return mixed
136
     */
137
    public static function getAPI($email, $password, $username = null, $options = [])
138
    {
139
        $auto = new static();
140
141
        return $auto->newAPI($email, $password, $username, $options);
142
    }
143
144
    /**
145
     * Execute the full discovery chain of events in the correct sequence
146
     * until a valid response is received, or all methods have failed.
147
     *
148
     * @param string $email
149
     * @param string $password
150
     * @param string $username
151
     *
152
     * @return string The discovered settings
153
     */
154
    protected function discover($email, $password, $username)
155
    {
156
        $result = $this->tryTopLevelDomain($email, $password, $username);
157
158
        if ($result === false) {
159
            $result = $this->tryAutoDiscoverSubDomain($email, $password, $username);
160
        }
161
162
        if ($result === false) {
163
            $result = $this->trySubdomainUnauthenticatedGet($email, $password, $username);
164
        }
165
166
        if ($result === false) {
167
            $result = $this->trySRVRecord($email, $password, $username);
168
        }
169
170
        return $result;
171
    }
172
173
    /**
174
     * Perform an NTLM authenticated HTTPS POST to the top-level
175
     * domain of the email address.
176
     *
177
     * @param string $email
178
     * @param string $password
179
     * @param string $username
180
     *
181
     * @return string The discovered settings
182
     */
183 View Code Duplication
    protected function tryTopLevelDomain($email, $password, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
    {
185
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
186
        $url = 'https://www.' . $topLevelDomain . self::AUTODISCOVER_PATH;
187
188
        return $this->doNTLMPost($url, $email, $password, $username);
189
    }
190
191
    /**
192
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
193
     * subdomain of the email address' TLD.
194
     *
195
     * @param string $email
196
     * @param string $password
197
     * @param string $username
198
     *
199
     * @return string The discovered settings
200
     */
201 View Code Duplication
    protected function tryAutoDiscoverSubDomain($email, $password, $username)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
202
    {
203
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
204
        $url = 'https://autodiscover.' . $topLevelDomain . self::AUTODISCOVER_PATH;
205
206
        return $this->doNTLMPost($url, $email, $password, $username);
207
    }
208
209
    /**
210
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
211
     * via 302 to the correct location to perform the HTTPS POST.
212
     *
213
     * @param string $email
214
     * @param string $password
215
     * @param string $username
216
     *
217
     * @return string The discovered settings
218
     */
219
    protected function trySubdomainUnauthenticatedGet($email, $password, $username)
220
    {
221
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
222
223
        $url = 'http://autodiscover.' . $topLevelDomain . self::AUTODISCOVER_PATH;
224
225
        $client = $this->httpPlayback->getHttpClient();
226
        $postOptions = [
227
            'timeout' => 2,
228
            'allow_redirects' => false,
229
            'headers' => [
230
                'Content-Type' => 'text/xml; charset=utf-8'
231
            ],
232
            'curl' => []
233
        ];
234
235
        try {
236
            $response = $client->get($url, $postOptions);
237
238
            if ($response->getStatusCode() == 301 || $response->getStatusCode() == 302) {
239
                return $this->doNTLMPost($response->getHeaderLine('Location'), $email, $password, $username);
240
            }
241
        } catch (\Exception $e) {
242
        }
243
244
        return false;
245
    }
246
247
    /**
248
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
249
     *
250
     * @link http://support.microsoft.com/kb/940881
251
     *
252
     * @param string $email
253
     * @param string $password
254
     * @param string $username
255
     *
256
     * @return string The discovered settings
257
     */
258
    protected function trySRVRecord($email, $password, $username)
259
    {
260
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
261
        $srvHost = '_autodiscover._tcp.' . $topLevelDomain;
262
        $lookup = dns_get_record($srvHost, DNS_SRV);
263
        if (sizeof($lookup) > 0) {
264
            $host = $lookup[0]['target'];
265
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
266
267
            return $this->doNTLMPost($url, $email, $password, $username);
268
        }
269
270
        return false;
271
    }
272
273
    /**
274
     * Perform the NTLM authenticated post against one of the chosen
275
     * endpoints.
276
     *
277
     * @param string $url URL to try posting to
278
     * @param string $email
279
     * @param string $password
280
     * @param string $username
281
     *
282
     * @return string The discovered settings
283
     */
284
    protected function doNTLMPost($url, $email, $password, $username)
285
    {
286
        $client = $this->httpPlayback->getHttpClient();
287
        $autodiscoverXml = <<<XML
288
<?xml version="1.0" encoding="UTF-8"?>
289
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
290
 <Request>
291
  <EMailAddress>$email</EMailAddress>
292
  <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
293
 </Request>
294
</Autodiscover>
295
XML;
296
        $postOptions = [
297
            'body' => $autodiscoverXml,
298
            'timeout' => 2,
299
            'allow_redirects' => true,
300
            'headers' => [
301
                'Content-Type' => 'text/xml; charset=utf-8'
302
            ],
303
            'curl' => []
304
        ];
305
        $auth = ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password);
306
        $postOptions = array_replace_recursive($postOptions, $auth);
307
308
        try {
309
            $response = $client->post($url, $postOptions);
310
        } catch (\Exception $e) {
311
            return false;
312
        }
313
314
        return $this->parseAutodiscoverResponse($response->getBody()->__toString());
315
    }
316
317
    /**
318
     * Parse the Autoresponse Payload, particularly to determine if an
319
     * additional request is necessary.
320
     *
321
     * @param $response
322
     * @return array|bool
323
     * @throws AutoDiscoverFailed
324
     */
325
    protected function parseAutodiscoverResponse($response)
326
    {
327
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
328
        if (substr($response, 0, 5) !== '<?xml') {
329
            throw new AutoDiscoverFailed();
330
        }
331
332
        $response = $this->responseToArray($response);
333
334
        if (isset($response['Error'])) {
335
            return false;
336
        }
337
338
        $action = $response['Account']['Action'];
339
        if ($action == 'redirectUrl' || $action == 'redirectAddr') {
340
            return false;
341
        }
342
343
        return $response;
344
    }
345
346
    /**
347
     * Get a top level domain based on an email address
348
     *
349
     * @param $email
350
     * @return bool|string
351
     */
352
    protected function getTopLevelDomainFromEmail($email)
353
    {
354
        $pos = strpos($email, '@');
355
        if ($pos !== false) {
356
            return trim(substr($email, $pos + 1));
357
        }
358
359
        return false;
360
    }
361
362
    /**
363
     * Utility function to parse XML payloads from the response into easier
364
     * to manage associative arrays.
365
     *
366
     * @param string $xml XML to parse
367
     * @return array
368
     */
369
    protected function responseToArray($xml)
370
    {
371
        $xml = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA);
372
373
        return json_decode(json_encode($xml), true)['Response'];
374
    }
375
}
376