Completed
Push — master ( ac6956...53001c )
by Gareth
03:20
created

EWSAutodiscover::setCertificateAuthorityPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 6
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
                'name' => 'VERSION_2007',
45
                'spCount' => 3
46 4
            ],
47
            14 => [
48
                'name' => 'VERSION_2010',
49
                'spCount' => 3
50
            ],
51
            15 => [
52
                'name' => 'VERSION_2013',
53
                'spCount' => 1
54
            ]
55
        ];
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
        }
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 === false) {
85
            throw new AutoDiscoverFailed();
86
        }
87
88
        $server = false;
89
        $version = null;
90
91
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
92
        foreach ($settings['Account']['Protocol'] as $protocol) {
93
            if (($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
94
                && isset($protocol['ServerVersion'])
95
            ) {
96
                $serverVersion = $this->parseServerVersion($protocol['ServerVersion']);
97
                if ($serverVersion) {
98
                    $version = $serverVersion;
99
                }
100
            }
101
102
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
103
                $server = $protocol['Server'];
104
            }
105
        }
106
107
        if (!$server) {
108
            throw new AutoDiscoverFailed();
109
        }
110
111
        $options = [];
112
        if ($version !== null) {
113
            $options['version'] = $version;
114
        }
115
116
        return API::withUsernameAndPassword($server, $email, $password, $options);
117
    }
118
119
    /**
120
     * Static method may fail if there are issues surrounding SSL certificates.
121
     * In such cases, set up the object as needed, and then call newEWS().
122
     *
123
     * @param string $email
124
     * @param string $password
125
     * @param string $username If left blank, the email provided will be used.
126
     * @return mixed
127
     */
128
    public static function getAPI($email, $password, $username = null, $options = [])
129
    {
130
        $auto = new static();
131
132
        return $auto->newAPI($email, $password, $username, $options);
133
    }
134
135
    /**
136
     * Execute the full discovery chain of events in the correct sequence
137
     * until a valid response is received, or all methods have failed.
138
     *
139
     * @param string $email
140
     * @param string $password
141
     * @param string $username
142
     *
143
     * @return string The discovered settings
144
     */
145
    protected function discover($email, $password, $username)
146
    {
147
        $result = $this->tryTopLevelDomain($email, $password, $username);
148
149
        if ($result === false) {
150
            $result = $this->tryAutoDiscoverSubDomain($email, $password, $username);
151
        }
152
153
        if ($result === false) {
154
            $result = $this->trySubdomainUnauthenticatedGet($email, $password, $username);
155
        }
156
157
        if ($result === false) {
158
            $result = $this->trySRVRecord($email, $password, $username);
159
        }
160
161
        return $result;
162
    }
163
164
    /**
165
     * Perform an NTLM authenticated HTTPS POST to the top-level
166
     * domain of the email address.
167
     *
168
     * @param string $email
169
     * @param string $password
170
     * @param string $username
171
     *
172
     * @return string The discovered settings
173
     */
174 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...
175
    {
176
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
177
        $url = 'https://www.' . $topLevelDomain . self::AUTODISCOVER_PATH;
178
179
        return $this->doNTLMPost($url, $email, $password, $username);
180
    }
181
182
    /**
183
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
184
     * subdomain of the email address' TLD.
185
     *
186
     * @param string $email
187
     * @param string $password
188
     * @param string $username
189
     *
190
     * @return string The discovered settings
191
     */
192 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...
193
    {
194
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
195
        $url = 'https://autodiscover.' . $topLevelDomain . self::AUTODISCOVER_PATH;
196
197
        return $this->doNTLMPost($url, $email, $password, $username);
198
    }
199
200
    /**
201
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
202
     * via 302 to the correct location to perform the HTTPS POST.
203
     *
204
     * @param string $email
205
     * @param string $password
206
     * @param string $username
207
     *
208
     * @return string The discovered settings
209
     */
210
    protected function trySubdomainUnauthenticatedGet($email, $password, $username)
211
    {
212
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
213
214
        $url = 'http://autodiscover.' . $topLevelDomain . self::AUTODISCOVER_PATH;
215
216
        $client = $this->httpPlayback->getHttpClient();
217
        $postOptions = [
218
            'timeout' => 2,
219
            'allow_redirects' => false,
220
            'headers' => [
221
                'Content-Type' => 'text/xml; charset=utf-8'
222
            ],
223
            'curl' => []
224
        ];
225
226
        try {
227
            $response = $client->get($url, $postOptions);
228
229
            if ($response->getStatusCode() == 301 || $response->getStatusCode() == 302) {
230
                return $this->doNTLMPost($response->getHeaderLine('Location'), $email, $password, $username);
231
            }
232
        } catch (\Exception $e) {
1 ignored issue
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
233
        }
234
235
        return false;
236
    }
237
238
    /**
239
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
240
     *
241
     * @link http://support.microsoft.com/kb/940881
242
     *
243
     * @param string $email
244
     * @param string $password
245
     * @param string $username
246
     *
247
     * @return string The discovered settings
248
     */
249
    protected function trySRVRecord($email, $password, $username)
250
    {
251
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
252
        $srvHost = '_autodiscover._tcp.' . $topLevelDomain;
253
        $lookup = dns_get_record($srvHost, DNS_SRV);
254
        if (sizeof($lookup) > 0) {
255
            $host = $lookup[0]['target'];
256
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
257
258
            return $this->doNTLMPost($url, $email, $password, $username);
259
        }
260
261
        return false;
262
    }
263
264
    /**
265
     * Perform the NTLM authenticated post against one of the chosen
266
     * endpoints.
267
     *
268
     * @param string $url URL to try posting to
269
     * @param string $email
270
     * @param string $password
271
     * @param string $username
272
     *
273
     * @return string The discovered settings
274
     */
275
    protected function doNTLMPost($url, $email, $password, $username)
276
    {
277
        $client = $this->httpPlayback->getHttpClient();
278
        $autodiscoverXml = <<<XML
279
<?xml version="1.0" encoding="UTF-8"?>
280
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
281
 <Request>
282
  <EMailAddress>$email</EMailAddress>
283
  <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
284
 </Request>
285
</Autodiscover>
286
XML;
287
        $postOptions = [
288
            'body' => $autodiscoverXml,
289
            'timeout' => 2,
290
            'allow_redirects' => true,
291
            'headers' => [
292
                'Content-Type' => 'text/xml; charset=utf-8'
293
            ],
294
            'curl' => []
295
        ];
296
        $auth = ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password);
297
        $postOptions = array_replace_recursive($postOptions, $auth);
298
        
299
        try {
300
            $response = $client->post($url, $postOptions);
301
        } catch (\Exception $e) {
302
            return false;
303
        }
304
305
        return $this->parseAutodiscoverResponse($response->getBody()->__toString());
306
    }
307
308
    /**
309
     * Parse the Autoresponse Payload, particularly to determine if an
310
     * additional request is necessary.
311
     *
312
     * @param $response
313
     * @return array|bool
314
     * @throws AutoDiscoverFailed
315
     */
316
    protected function parseAutodiscoverResponse($response)
317
    {
318
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
319
        if (substr($response, 0, 5) !== '<?xml') {
320
            throw new AutoDiscoverFailed();
321
        }
322
323
        $response = $this->responseToArray($response);
324
325
        if (isset($response['Error'])) {
326
            return false;
327
        }
328
329
        $action = $response['Account']['Action'];
330
        if ($action == 'redirectUrl' || $action == 'redirectAddr') {
331
            return false;
332
        }
333
334
        return $response;
335
    }
336
337
    /**
338
     * Get a top level domain based on an email address
339
     *
340
     * @param $email
341
     * @return bool|string
342
     */
343
    protected function getTopLevelDomainFromEmail($email)
344
    {
345
        $pos = strpos($email, '@');
346
        if ($pos !== false) {
347
            return trim(substr($email, $pos + 1));
348
        }
349
350
        return false;
351
    }
352
353
    /**
354
     * Utility function to parse XML payloads from the response into easier
355
     * to manage associative arrays.
356
     *
357
     * @param string $xml XML to parse
358
     * @return array
359
     */
360
    protected function responseToArray($xml)
361
    {
362
        $xml = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA);
363
        return json_decode(json_encode($xml), true)['Response'];
364
    }
365
}
366