Passed
Branch master (6fbc1b)
by Gareth
10:31
created

ExchangeWebServices   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 441
Duplicated Lines 0 %

Test Coverage

Coverage 87.42%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
eloc 149
dl 0
loc 441
ccs 139
cts 159
cp 0.8742
rs 8.5599
c 14
b 0
f 0
wmc 48

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getPrimarySmtpMailbox() 0 3 1
A getVersion() 0 3 1
A drillDownResponseLevels() 0 20 5
A cleanServerUrl() 0 20 5
A __call() 0 6 1
A setPrimarySmtpEmailAddress() 0 7 1
A getPrimarySmtpEmailAddress() 0 7 2
A executeMiddlewareStack() 0 25 3
A fromUsernameAndPassword() 0 7 1
A buildMiddlewareStack() 0 20 2
A fromCallbackToken() 0 7 1
A fromCustomAuthentication() 0 7 1
A processResponse() 0 18 3
A createClient() 0 39 5
A getClient() 0 3 1
A getServer() 0 3 1
A getItemsFromResponse() 0 21 5
A __construct() 0 11 2
A setTimezone() 0 3 1
A handleNonSuccessfulResponses() 0 19 6

How to fix   Complexity   

Complex Class

Complex classes like ExchangeWebServices often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ExchangeWebServices, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Contains ExchangeWebServices.
4
 */
5
6
7
namespace garethp\ews\API;
8
9
use garethp\ews\API\Exception\ExchangeException;
10
use garethp\ews\API\Exception\NoResponseReturnedException;
11
use garethp\ews\API\Exception\ServiceUnavailableException;
12
use garethp\ews\API\Exception\UnauthorizedException;
13
use garethp\ews\API\ExchangeWebServices\MiddlewareFactory;
14
use garethp\ews\API\Message\ResponseMessageType;
15
use garethp\ews\API\Type\EmailAddressType;
16
use \Closure;
17
18
/**
19
 * Base class of the Exchange Web Services application.
20
 *
21
 *
22
 *
23
 * @package php-ews\Client
24
 *
25
 * @method Type AddDelegate($request)
26
 * @method Type ApplyConversationAction($request)
27
 * @method Type ConvertId($request)
28
 * @method Type CopyFolder($request)
29
 * @method Type CopyItem($request)
30
 * @method Type CreateAttachment($request)
31
 * @method Type CreateFolder($request)
32
 * @method Type CreateItem($request)
33
 * @method Type CreateManagedFolder($request)
34
 * @method Type CreateUserConfiguration($request)
35
 * @method Type DeleteAttachment($request)
36
 * @method Type DeleteFolder($request)
37
 * @method Type DeleteItem($request)
38
 * @method Type DeleteUserConfiguration($request)
39
 * @method Type DisconnectPhoneCall($request)
40
 * @method Type EmptyFolder($request)
41
 * @method Type ExpandDL($request)
42
 * @method Type ExportItems($request)
43
 * @method Type FindConversation($request)
44
 * @method Type FindFolder($request)
45
 * @method Type FindItem($request)
46
 * @method Type FindMessageTrackingReport($request)
47
 * @method Type GetAttachment($request)
48
 * @method Type GetDelegate($request)
49
 * @method Type GetEvents($request)
50
 * @method Type GetFolder($request)
51
 * @method Type GetInboxRules($request)
52
 * @method Type GetItem($request)
53
 * @method Type GetMailTips($request)
54
 * @method Type GetMessageTrackingReport($request)
55
 * @method Type GetPasswordExpirationDate($request)
56
 * @method Type GetPhoneCallInformation($request)
57
 * @method Type GetRoomLists($request)
58
 * @method Type GetRooms($request)
59
 * @method Type GetServerTimeZones($request)
60
 * @method Type GetServiceConfiguration($request)
61
 * @method Type GetSharingFolder($request)
62
 * @method Type GetSharingMetadata($request)
63
 * @method Type GetStreamingEvents($request)
64
 * @method Type GetUserAvailability($request)
65
 * @method Type GetUserConfiguration($request)
66
 * @method Type GetUserOofSettings($request)
67
 * @method Type MoveFolder($request)
68
 * @method Type MoveItem($request)
69
 * @method Type PlayOnPhone($request)
70
 * @method Type RefreshSharingFolder($request)
71
 * @method Type RemoveDelegate($request)
72
 * @method Type ResolveNames($request)
73
 * @method Type SendItem($request)
74
 * @method Type SetUserOofSettings($request)
75
 * @method Type Subscribe($request)
76
 * @method Type SyncFolderHierarchy($request)
77
 * @method Type SyncFolderItems($request)
78
 * @method Type Unsubscribe($request)
79
 * @method Type UpdateDelegate($request)
80
 * @method Type UpdateFolder($request)
81
 * @method Type UpdateInboxRules($request)
82
 * @method Type UpdateItem($request)
83
 * @method Type UpdateUserConfiguration($request)
84
 * @method Type UploadItems($request)
85
 */
86
class ExchangeWebServices
87
{
88
    const VERSION_2007 = 'Exchange2007';
89
90
    const VERSION_2007_SP1 = 'Exchange2007_SP1';
91
92
    const VERSION_2010 = 'Exchange2010';
93
94
    const VERSION_2010_SP1 = 'Exchange2010_SP1';
95
96
    const VERSION_2010_SP2 = 'Exchange2010_SP2';
97
98
    const VERSION_2013 = 'Exchange2013';
99
100
    const VERSION_2013_SP1 = 'Exchange2013_SP1';
101
102
    const VERSION_2016 = 'Exchange2016';
103
104
    /**
105
     * Password to use when connecting to the Exchange server.
106
     *
107
     * @var string
108
     */
109
    protected $password = null;
110
111
    /**
112
     * Location of the Exchange server.
113
     *
114
     * @var string
115
     */
116
    protected $server = null;
117
118
    /**
119
     * SOAP client used to make the request
120
     *
121
     * @var NTLMSoapClient
122
     */
123
    protected $soap = null;
124
125
    /**
126
     * Username to use when connecting to the Exchange server.
127
     *
128
     * @var string
129
     */
130
    protected $username = null;
131
132
    /**
133
     * @var EmailAddressType
134
     */
135
    protected $primarySmtpMailbox = null;
136
137
    /**
138
     * @var Callable[]
139
     */
140
    protected static $middlewareStack = false;
141
142
    /**
143
     * A setting to check whether or not responses should be drilled down before being
144
     * returned. Setting this to false
145
     * will return the raw responses without any filtering
146
     *
147
     * @var bool
148
     */
149
    protected $drillDownResponses = true;
150
151
    /**
152
     * Miscrosoft Exchange version that we are going to connect to
153
     *
154
     * @var string
155
     */
156
    protected $version = null;
157
158
    protected $options = null;
159
160
    /**
161
     * The timezone for the client
162
     *
163
     * @var bool
164
     */
165
    protected $timezone = false;
166
167
    /**
168
     * @return EmailAddressType
169
     */
170 28
    public function getPrimarySmtpMailbox()
171
    {
172 28
        return $this->primarySmtpMailbox;
173
    }
174
175 1
    public function getPrimarySmtpEmailAddress()
176
    {
177 1
        if ($this->primarySmtpMailbox == null) {
178 1
            return null;
179
        }
180
181 1
        return $this->primarySmtpMailbox->getEmailAddress();
182
    }
183
184 2
    public function setPrimarySmtpEmailAddress($emailAddress)
185
    {
186 2
        $mailbox = new EmailAddressType();
187 2
        $mailbox->setEmailAddress($emailAddress);
188 2
        $this->primarySmtpMailbox = $mailbox;
189
190 2
        return $this;
191
    }
192
193
    /**
194
     * @param boolean $timezone
195
     */
196
    public function setTimezone($timezone)
197
    {
198
        $this->timezone = $timezone;
199
    }
200
201
    /**
202
     * @return string
203
     */
204
    public function getVersion()
205
    {
206
        return $this->version;
207
    }
208
209
    /**
210
     * @return string
211
     */
212
    public function getServer()
213
    {
214
        return $this->server;
215
    }
216
217
    /**
218
     * Constructor for the ExchangeWebServices class
219
     *
220
     * @param string $server
221
     * @param string $username
222
     * @param string $password
223
     * @param array $options
224
     */
225 37
    protected function __construct($server = null, $username = null, $password = null, $options = array())
226
    {
227 37
        if ($server !== null) {
228
            $this->createClient(
229
                $server,
230
                ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password),
231
                $options
232
            );
233
        }
234
235 37
        $this->buildMiddlewareStack();
236 37
    }
237
238 35
    public static function fromUsernameAndPassword($server, $username, $password, $options)
239
    {
240 35
        $self = new self();
241 35
        $self->createClient($server, ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password), $options);
242 35
        $self->options = $options;
243
244 35
        return $self;
245
    }
246
247 1
    public static function fromCallbackToken($server, $token, $options)
248
    {
249 1
        $self = new self();
250 1
        $self->createClient($server, ExchangeWebServicesAuth::fromCallbackToken($token), $options);
251 1
        $self->options = $options;
252
253 1
        return $self;
254
    }
255
256 1
    public static function fromCustomAuthentication($server, $authentication, $options)
257
    {
258 1
        $self = new self();
259 1
        $self->createClient($server, $authentication, $options);
260 1
        $self->options = $options;
261
262 1
        return $self;
263
    }
264
265 37
    protected function createClient($server, $auth, $options)
266
    {
267 37
        $location = 'https://' . $this->cleanServerUrl($server) . '/EWS/Exchange.asmx';
268
269 37
        $options = array_replace_recursive([
270 37
            'version' => self::VERSION_2007,
271 37
            'trace' => 1,
272 37
            'exceptions' => true,
273 37
            'classmap' => ClassMap::getClassMap(),
274
            'drillDownResponses' => true
275 37
        ], $options);
276
277 37
        $this->server = $server;
278 37
        $this->version = $options['version'];
279
280 37
        if (version_compare(PHP_VERSION, '8', '<')) {
281 37
            $backup = libxml_disable_entity_loader(false);
282 37
        }
283 37
284 37
        $this->soap = new NTLMSoapClient(
285
            $location,
286 37
            $auth,
287 37
            dirname(__FILE__) . '/../../Resources/wsdl/services.wsdl',
288
            $options
289 37
        );
290 1
291 1
        if (version_compare(PHP_VERSION, '8', '<')) {
292
            libxml_disable_entity_loader($backup);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $backup does not seem to be defined for all execution paths leading up to this point.
Loading history...
293 37
        }
294 1
295 1
        if (isset($options['primarySmtpEmailAddress'])) {
296
            $this->setPrimarySmtpEmailAddress($options['primarySmtpEmailAddress']);
297 37
        }
298 37
299
        if (isset($options['impersonation'])) {
300
            $this->setPrimarySmtpEmailAddress($options['impersonation']);
301
        }
302
303
        $this->drillDownResponses = $options['drillDownResponses'];
304
    }
305
306
    /**
307
     * @codeCoverageIgnore
308
     *
309
     * @param $name
310
     * @param $arguments
311
     * @return Type
312
     * @throws \garethp\ews\API\Exception
313
     */
314
    public function __call($name, $arguments)
315
    {
316
        $request = MiddlewareRequest::newRequest($name, $arguments, $this->options);
317
        $response = $this->executeMiddlewareStack(self::$middlewareStack, $request);
318
        $response = $response->getResponse();
319
        return $response;
320
    }
321 33
322
    /**
323 33
     * Returns the SOAP Client that may be used to make calls against the server
324
     *
325
     * @return NTLMSoapClient
326
     */
327
    public function getClient()
328
    {
329
        return $this->soap;
330
    }
331
332 44
    /**
333
     * Cleans the server URL for usage
334 44
     *
335 44
     * @param $server
336 39
     * @return string
337 39
     */
338 39
    public function cleanServerUrl($server)
339
    {
340 44
        $url = parse_url($server);
341 44
        if (!isset($url['host']) && isset($url['path'])) {
342 2
            $url['host'] = $url['path'];
343 2
            unset($url['path']);
344
        }
345 44
346 4
        $server = $url['host'];
347 4
        if (isset($url['port'])) {
348
            $server .= ':' . $url['port'];
349 44
        }
350
351 44
        if (isset($url['path'])) {
352
            $server .= $url['path'];
353
        }
354
355
        $server = rtrim($server, "/");
356
357
        return $server;
358
    }
359
360
    /**
361
     * Process a response to verify that it succeeded and take the appropriate
362 30
     * action
363
     *
364
     * @param \garethp\ews\API\Message\BaseResponseMessageType $response
365 30
     * @return Type[]
366 30
     * @throws \garethp\ews\API\Exception
367
     */
368 30
    protected function processResponse($response)
369
    {
370
        // If the soap call failed then we need to thow an exception.
371
        $code = $this->getClient()->getResponseCode();
372 30
        $this->handleNonSuccessfulResponses($response, $code);
373
374
        if (!$this->drillDownResponses) {
375
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type garethp\ews\API\Message\BaseResponseMessageType which is incompatible with the documented return type garethp\ews\API\Type[].
Loading history...
376 30
        }
377 30
378
        if (!$response->exists('responseMessages')) {
379 30
            return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type garethp\ews\API\Message\BaseResponseMessageType which is incompatible with the documented return type garethp\ews\API\Type[].
Loading history...
380
        }
381
382
        $response = $response->getResponseMessages();
383
        $response = $this->drillDownResponseLevels($response);
384
385
        return $response;
386
    }
387 30
388
    /**
389 30
     * @param $response
390
     * @return array
391 30
     * @throws \garethp\ews\API\Exception
392 30
     */
393 30
    public static function drillDownResponseLevels($response)
394 30
    {
395 30
        $items = self::getItemsFromResponse($response);
396
397 30
        if (count($items) === 1) {
398
            reset($items);
399
            $key = key($items);
400 30
            $methodName = "get$key";
401
            $response = $response->$methodName();
402 3
403 3
            return self::drillDownResponseLevels($response);
404
        }
405
406 30
        if (is_array($items) && isset($items[1]) && $items[1] instanceof Message\ResponseMessageType) {
407
            return array_map(function ($responseItem) {
408
                return self::drillDownResponseLevels($responseItem);
409
            }, $items);
410
        }
411
412
        return $response;
413
    }
414 30
415
    /**
416 30
     * @param $response
417 30
     * @return array
418 30
     * @throws ExchangeException
419 30
     */
420
    protected static function getItemsFromResponse($response)
421 30
    {
422 3
        $items = array();
423 3
        if ($response instanceof Type) {
424
            $items = $response->getNonNullItems();
425 30
        }
426 30
427 1
        if (is_array($response)) {
428
            $items = $response;
429
        }
430 30
431 30
        if ($response instanceof Message\ResponseMessageType) {
432 30
            if ($response->getResponseClass() !== "Success") {
433
                throw new ExchangeException($response);
434 30
            }
435
436
            unset($items['responseClass']);
437
            unset($items['responseCode']);
438
        }
439
440
        return $items;
441
    }
442
443
    /**
444
     * @param Message\BaseResponseMessageType $response
445 30
     * @param $code
446
     * @throws ExchangeException
447 30
     * @throws NoResponseReturnedException
448
     * @throws ServiceUnavailableException
449
     * @throws UnauthorizedException
450
     */
451 30
    protected function handleNonSuccessfulResponses($response, $code)
452
    {
453
        if ($code == 401) {
454
            throw new UnauthorizedException();
455 30
        }
456
457
        if ($code == 503) {
458
            throw new ServiceUnavailableException();
459
        }
460
461
        if ($code >= 300) {
462 30
            $response = new ResponseMessageType();
463
            $response->setMessageText('SOAP client returned status of ' . $code);
464
465 30
            throw new ExchangeException($response, $code);
466
        }
467 37
468
        if (empty($response) || empty($response->getNonNullResponseMessages())) {
469 37
            throw new NoResponseReturnedException();
470 1
        }
471
    }
472 1
473
    protected function buildMiddlewareStack()
474 1
    {
475
        if (self::$middlewareStack === false) {
0 ignored issues
show
introduced by
The condition self::middlewareStack === false is always false.
Loading history...
476
            $factory = new MiddlewareFactory();
477 1
478
            self::$middlewareStack = [
479
                //Make the actual SOAP call
480 1
                $factory->getSoapCall(),
481
482
                //Transform an object of type Type to an XML Object
483 1
                $factory->getTypeToXMLObject(),
484
485
                //The SyncScope option isn't available for Exchange 2007 SP1 and below
486 1
                $factory->getStripSyncScopeForExchange2007(),
487 1
488 1
                //Add response processing
489 37
                $factory->getProcessResponse(),
490
491
                //Adds last request to FindFolder and FindItem responses
492
                $factory->getAddLastRequestToPagedResults()
493
            ];
494
        }
495
    }
496 30
497
    /**
498 30
     * @param array $middlewareStack
499 30
     * @param MiddlewareRequest $request
500
     * @return MiddlewareResponse
501
     */
502 30
    protected function executeMiddlewareStack(array $middlewareStack, MiddlewareRequest $request)
503
    {
504 30
        $newStack = [];
505 30
        foreach ($middlewareStack as $key => $current) {
506 30
            /** @var $current callable */
507
            $last = function () {
508 30
            };
509 30
510 30
            if ($key != 0) {
511
                $last = $newStack[$key - 1];
512 30
            }
513
514
            $current = Closure::bind($current, $this, $this);
515 30
            $newStack[] = function (MiddlewareRequest $request) use ($current, $last) {
516
                return $current($request, $last);
517 30
            };
518
        }
519
520 30
        /** @var $newStack callable[] */
521
        $newStack = array_reverse($newStack);
522
523
        $top = $newStack[0];
524
525
        /** @var $top callable */
526
        return $top($request);
527
    }
528
}
529