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