ExchangeWebServices   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 445
Duplicated Lines 0 %

Test Coverage

Coverage 85.53%

Importance

Changes 14
Bugs 0 Features 0
Metric Value
eloc 152
c 14
b 0
f 0
dl 0
loc 445
ccs 130
cts 152
cp 0.8553
rs 8.48
wmc 49

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getPrimarySmtpMailbox() 0 3 1
A getVersion() 0 3 1
A cleanServerUrl() 0 20 5
A __call() 0 6 1
A setPrimarySmtpEmailAddress() 0 7 1
A getPrimarySmtpEmailAddress() 0 7 2
A fromUsernameAndPassword() 0 7 1
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 __construct() 0 11 2
A setTimezone() 0 3 1
A drillDownResponseLevels() 0 24 6
A executeMiddlewareStack() 0 25 3
A buildMiddlewareStack() 0 20 2
A getItemsFromResponse() 0 21 5
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;
0 ignored issues
show
Bug introduced by
The type \Closure was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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