Completed
Push — master ( b64fc1...5651bd )
by Raffael
16:20 queued 08:39
created

MicrosoftGraph::getOne()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 36
ccs 0
cts 21
cp 0
rs 8.7217
c 0
b 0
f 0
cc 6
nc 8
nop 2
crap 42
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * tubee
7
 *
8
 * @copyright   Copryright (c) 2017-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Tubee\Endpoint;
13
14
use Generator;
15
use GuzzleHttp\Client;
16
use GuzzleHttp\Exception\RequestException;
17
use Psr\Log\LoggerInterface;
18
use Tubee\AttributeMap\AttributeMapInterface;
19
use Tubee\Collection\CollectionInterface;
20
use Tubee\Endpoint\MicrosoftGraph\Exception as GraphException;
21
use Tubee\Endpoint\OdataRest\QueryTransformer;
22
use Tubee\EndpointObject\EndpointObjectInterface;
23
use Tubee\Workflow\Factory as WorkflowFactory;
24
25
class MicrosoftGraph extends OdataRest
26
{
27
    /**
28
     * Kind.
29
     */
30
    public const KIND = 'MicrosoftGraphEndpoint';
31
32
    /**
33
     * API Root Endpoint.
34
     */
35
    public const API_ENDPOINT = 'https://graph.microsoft.com/v1.0';
36
37
    /**
38
     * Batch Endpoint.
39
     */
40
    public const BATCH_ENDPOINT = 'https://graph.microsoft.com/v1.0/$batch';
41
42
    /**
43
     * Graph API batch size limit.
44
     */
45
    public const BATCH_SIZE = 20;
46
47
    /**
48
     * Init endpoint.
49
     */
50
    public function __construct(string $name, string $type, Client $client, CollectionInterface $collection, WorkflowFactory $workflow, LoggerInterface $logger, array $resource = [])
51
    {
52
        $this->container = 'value';
53
        parent::__construct($name, $type, $client, $collection, $workflow, $logger, $resource);
54
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function transformQuery(?array $query = null)
60
    {
61
        if ($this->filter_all !== null) {
62
            return QueryTransformer::transform($this->getFilterAll());
63
        }
64
        if (!empty($query)) {
65
            if ($this->filter_all === null) {
66
                return QueryTransformer::transform($query);
67
            }
68
69
            return QueryTransformer::transform([
70
                    '$and' => [
71
                        $this->getFilterAll(),
72
                        $query,
73
                    ],
74
                ]);
75
        }
76
77
        return null;
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function getAll(?array $query = null): Generator
84
    {
85
        $options = $this->getRequestOptions();
86
        $query = $this->transformQuery($query);
87
        $this->logGetAll($query);
88
89
        if ($query !== null) {
90
            $options['query']['$filter'] = $query;
91
        }
92
93
        $i = 0;
94
        $response = $this->client->get('', $options);
95
        $data = $this->getResponse($response);
96
97
        foreach ($data as $object) {
98
            if ($this->isGroupEndpoint()) {
99
                $object = array_merge($object, $this->fetchMembers($object['id']));
100
            }
101
102
            yield $this->build($object);
103
        }
104
105
        return $i;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function create(AttributeMapInterface $map, array $object, bool $simulate = false): ?string
112
    {
113
        $requests = [];
114
        $new = $object;
115
116
        unset($object['owners'], $object['members']);
117
118
        $id = parent::create($map, $object, $simulate);
119
120
        if ($this->isGroupEndpoint()) {
121
            $requests = $this->getMemberChangeBatchRequests($id, $new, []);
122
        }
123
124
        if ($this->isGroupEndpoint() && isset($object['resourceProvisioningOptions']) && in_array('Team', $object['resourceProvisioningOptions'])) {
125
            $requests[] = [
126
                'id' => 'create-team',
127
                'url' => 'groups/'.$id.'/team',
128
                'method' => 'PUT',
129
                'body' => new \stdClass(),
130
                'headers' => [
131
                    'Content-Type' => 'application/json',
132
                ],
133
            ];
134
        }
135
136
        if (count($requests) !== 0) {
137
            $this->batch($requests, $simulate);
138
        }
139
140
        return $id;
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146
    public function change(AttributeMapInterface $map, array $diff, array $object, array $endpoint_object, bool $simulate = false): ?string
147
    {
148
        $id = $this->getResourceId($object, $endpoint_object);
149
        $uri = $this->client->getConfig('base_uri').'/'.$id;
150
        $requests = [];
151
152
        if ($this->isGroupEndpoint()) {
153
            if (isset($object['resourceProvisioningOptions'])
154
              && in_array('Team', $object['resourceProvisioningOptions'])
155
              && isset($endpoint_object['resourceProvisioningOptions'])
156
              && !in_array('Team', $endpoint_object['resourceProvisioningOptions'])) {
157
                $requests[] = [
158
                    'id' => 'create-team',
159
                    'method' => 'PUT',
160
                    'url' => '/groups/'.$id.'/team',
161
                    'body' => new \stdClass(),
162
                    'headers' => [
163
                        'Content-Type' => 'application/json',
164
                    ],
165
                ];
166
            }
167
168
            $requests = array_merge($requests, $this->getMemberChangeBatchRequests($id, $diff, $endpoint_object));
169
            unset($diff['members'], $diff['owners']);
170
        }
171
172
        if (count($requests) === 0) {
173
            $this->logChange($uri, $diff);
174
175
            if (count($diff) !== 0 && $simulate === false) {
176
                $this->client->patch($uri, $this->getRequestOptions([
177
                    'json' => $diff,
178
                ]));
179
            }
180
        } else {
181
            $request = [];
182
183
            if (count($diff) !== 0) {
184
                $request = [[
185
                    'id' => 'change',
186
                    'method' => 'PATCH',
187
                    'url' => substr($uri, strlen(self::API_ENDPOINT)),
188
                    'body' => $diff,
189
                    'headers' => [
190
                        'Content-Type' => 'application/json',
191
                    ],
192
                ]];
193
            }
194
195
            $this->logChange(self::BATCH_ENDPOINT, $requests);
196
            $this->batch(array_merge($request, $requests), $simulate);
197
        }
198
199
        return null;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function getOne(array $object, ?array $attributes = []): EndpointObjectInterface
206
    {
207
        $filter = $this->transformQuery($this->getFilterOne($object));
208
        $this->logGetOne($filter);
209
210
        $options = $this->getRequestOptions();
211
        $options['query']['$filter'] = $filter;
212
        $attributes[] = $this->identifier;
213
        $options['query']['$select'] = join(',', $attributes);
214
215
        try {
216
            $result = $this->client->get('', $options);
217
            $data = $this->getResponse($result);
218
        } catch (RequestException $e) {
0 ignored issues
show
Bug introduced by
The class GuzzleHttp\Exception\RequestException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
219
            if ($e->getCode() === 404) {
220
                throw new Exception\ObjectNotFound('no object found with filter '.$filter);
221
            }
222
223
            throw $e;
224
        }
225
226
        if (count($data) > 1) {
227
            throw new Exception\ObjectMultipleFound('found more than one object with filter '.$filter);
228
        }
229
        if (count($data) === 0) {
230
            throw new Exception\ObjectNotFound('no object found with filter '.$filter);
231
        }
232
233
        $data = array_shift($data);
234
235
        if ($this->isGroupEndpoint()) {
236
            $data = array_merge_recursive($data, $this->fetchMembers($data['id']));
237
        }
238
239
        return $this->build($data);
240
    }
241
242
    /**
243
     * Check if endpoint holds group objects.
244
     */
245
    protected function isGroupEndpoint(): bool
246
    {
247
        $url = (string) $this->client->getConfig('base_uri');
248
249
        if ($url === 'https://graph.microsoft.com/v1.0/groups' || $url === 'https://graph.microsoft.com/beta/groups') {
250
            return true;
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * Get member batch requests.
258
     */
259
    protected function getMemberChangeBatchRequests(string $id, array $diff, array $endpoint_object): array
260
    {
261
        $requests = [];
262
263
        foreach (['members', 'owners'] as $type) {
264
            if (!isset($diff[$type])) {
265
                $this->logger->info('attribute [{attribute}] not in diff (no update required), skip group member batching', [
266
                    'category' => get_class($this),
267
                    'attribute' => $type,
268
                ]);
269
270
                continue;
271
            }
272
273
            $add = $diff[$type];
274
            $remove = [];
275
276
            if (isset($endpoint_object[$type])) {
277
                $add = array_diff($diff[$type], $endpoint_object[$type]);
278
                $remove = array_diff($endpoint_object[$type], $diff[$type]);
279
            }
280
281
            foreach ($add as $member) {
282
                $requests[] = [
283
                    'id' => 'add-'.$type.'-'.$member,
284
                    'method' => 'POST',
285
                    'url' => '/groups/'.$id.'/'.$type.'/$ref',
286
                    'body' => [
287
                        '@odata.id' => self::API_ENDPOINT.'/directoryObjects/'.$member,
288
                    ],
289
                     'headers' => [
290
                        'Content-Type' => 'application/json',
291
                    ],
292
                ];
293
            }
294
295
            foreach ($remove as $member) {
296
                $requests[] = [
297
                    'id' => 'remove-'.$type.'-'.$member,
298
                    'method' => 'DELETE',
299
                    'url' => '/groups/'.$id.'/'.$type.'/'.$member.'/$ref',
300
                     'headers' => [
301
                        'Content-Type' => 'application/json',
302
                    ],
303
                ];
304
            }
305
        }
306
307
        return $requests;
308
    }
309
310
    /**
311
     * Get object url.
312
     */
313
    protected function getObjectUrl(array $objects): array
314
    {
315
        foreach ($objects as &$object) {
316
            $object = self::API_ENDPOINT.'/directoryObjects/'.$object;
317
        }
318
319
        return $objects;
320
    }
321
322
    /**
323
     * Batch call.
324
     */
325
    protected function batch(array $requests, bool $simulate = false, bool $throw = true): array
326
    {
327
        $results = [];
328
329
        foreach (array_chunk($requests, self::BATCH_SIZE) as $chunk) {
330
            $chunk = ['requests' => $chunk];
331
332
            $this->logger->debug('batch request chunk [{chunk}] ', [
333
                'category' => get_class($this),
334
                'chunk' => $chunk,
335
            ]);
336
337
            if ($simulate === false) {
338
                $response = $this->client->post(self::BATCH_ENDPOINT, $this->getRequestOptions([
339
                    'json' => $chunk,
340
                ]));
341
342
                $results = array_merge($results, $this->validateBatchResponse($response, $throw));
343
            }
344
        }
345
346
        return $results;
347
    }
348
349
    /**
350
     * Validate batch request.
351
     */
352
    protected function validateBatchResponse($response, bool $throw = true): array
353
    {
354
        $data = json_decode($response->getBody()->getContents(), true);
355
356
        if (!isset($data['responses'])) {
357
            throw new GraphException\BatchRequestFailed('invalid batch response data, expected responses list');
358
        }
359
360
        foreach ($data['responses'] as $result) {
361
            $this->logger->debug('validate batch request id [{id}]', [
362
                'category' => get_class($this),
363
                'id' => $result['id'],
364
            ]);
365
366
            if (isset($result['body']['error']) && $throw === true) {
367
                throw new GraphException\BatchRequestFailed('batch request part failed with error '.$result['body']['error']['message'].' and http code '.$result['status']);
368
            }
369
370
            $this->logger->debug('batch request part [{request}] succeeded with http code [{status}]', [
371
                'category' => get_class($this),
372
                'request' => $result['id'] ?? null,
373
                'status' => $result['status'] ?? null,
374
            ]);
375
        }
376
377
        return $data['responses'];
378
    }
379
380
    /**
381
     * Fetch members.
382
     */
383
    protected function fetchMembers(string $id): array
384
    {
385
        $requests = [
386
            [
387
                'id' => 'members',
388
                'method' => 'GET',
389
                'url' => '/groups/'.$id.'/members',
390
            ],
391
            [
392
                'id' => 'owners',
393
                'method' => 'GET',
394
                'url' => '/groups/'.$id.'/owners',
395
            ],
396
            [
397
                'id' => 'team',
398
                'method' => 'GET',
399
                'url' => '/groups/'.$id.'/team',
400
            ],
401
        ];
402
403
        $options = $this->getRequestOptions();
0 ignored issues
show
Unused Code introduced by
$options is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
404
        $this->logger->debug('fetch group members from batch request [{requests}]', [
405
            'class' => get_class($this),
406
            'requests' => $requests,
407
        ]);
408
409
        $data = $this->batch($requests, false, false);
410
411
        $set = [
412
            'owners' => [],
413
            'members' => [],
414
            'resourceProvisioningOptions' => [],
415
        ];
416
417
        foreach ($data as $response) {
418
            switch ($response['id']) {
419
                case 'owners':
420
                case 'members':
421
                default:
422
                    foreach ($response['body'][$this->container] as $record) {
423
                        $set[$response['id']][] = $record['id'];
424
                    }
425
426
                    $id = $response['id'];
427
                    $response = $response['body'];
428
                    while (isset($response['@odata.nextLink'])) {
429
                        $options = $this->getRequestOptions();
430
                        $response = $this->decodeResponse($this->client->get($response['@odata.nextLink'], $options));
431
                        $set[$id] = array_merge($set[$id], array_column($response[$this->container], 'id'));
432
                    }
433
434
                break;
435
                case 'team':
0 ignored issues
show
Unused Code introduced by
case 'team': if ($re...Team'; } break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
436
                    if ($response['status'] === 200) {
437
                        $set['resourceProvisioningOptions'][] = 'Team';
438
                    }
439
440
                break;
441
            }
442
        }
443
444
        return $set;
445
    }
446
}
447