Test Failed
Push — master ( 525695...4271e0 )
by Raffael
06:35 queued 03:26
created

Delta::findNodeAttributesWithCustomFilter()   C

Complexity

Conditions 10
Paths 32

Size

Total Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 68
ccs 0
cts 31
cp 0
rs 6.8315
c 0
b 0
f 0
cc 10
nc 32
nop 5
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
16
use Balloon\Filesystem\Node\Collection;
17
use Balloon\Filesystem\Node\NodeInterface;
18
use Balloon\Server\User;
19
use MongoDB\BSON\ObjectId;
20
use MongoDB\BSON\UTCDateTime;
21
use MongoDB\Database;
22
23
class Delta
24
{
25
    /**
26
     * Filesystem.
27
     *
28
     * @var Filesystem
29
     */
30
    protected $fs;
31
32
    /**
33
     * Db.
34
     *
35
     * @var Database
36
     */
37
    protected $db;
38
39
    /**
40
     * User.
41
     *
42
     * @var User
43
     */
44
    protected $user;
45
46
    /**
47
     * Acl.
48
     *
49
     * @var Acl
50
     */
51
    protected $acl;
52
53
    /**
54
     * Client.
55
     *
56
     * @var array
57
     */
58
    protected $client = [
59
        'type' => null,
60
        'app' => null,
61
        'v' => null,
62
        'hostname' => null,
63
    ];
64
65
    /**
66
     * Initialize delta.
67
     */
68
    public function __construct(Filesystem $fs, Database $db, Acl $acl)
69
    {
70
        $this->fs = $fs;
71
        $this->db = $db;
72
        $this->acl = $acl;
73
        $this->user = $fs->getUser();
74
        $this->parseClient();
75
    }
76
77
    /**
78
     * Add delta event.
79
     */
80
    public function add(string $event, NodeInterface $node, array $context = []): ObjectId
81
    {
82
        $context['operation'] = $event;
83
        $context['owner'] = $this->getEventOwner($node);
84
        $context['name'] = $node->getName();
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
85
        $context['node'] = $node->getId();
86
87
        if ($node->isShareMember()) {
88
            $context['share'] = $node->getShareId();
89
        }
90
91
        $context['timestamp'] = new UTCDateTime();
92
        $context['client'] = $this->client;
93
94
        $result = $this->db->delta->insertOne($context);
95
96
        return $result->getInsertedId();
97
    }
98
99
    /**
100
     * Build a single dimension array with all nodes.
101
     */
102
    public function buildFeedFromCurrentState(?array $cursor = null, int $limit = 100, ?NodeInterface $node = null): array
103
    {
104
        $current_cursor = 0;
105
        $filter = ['$and' => [
106
            ['$or' => [
107
                ['shared' => [
108
                    '$in' => $this->user->getShares(),
109
                ]],
110
                [
111
                    'shared' => ['$type' => 8],
112
                    'owner' => $this->user->getId(),
113
                ],
114
            ]],
115
            ['deleted' => false],
116
        ]];
117
118
        if (is_array($cursor)) {
119
            $current_cursor = $cursor[1];
120
        }
121
122
        $children = $this->findNodeAttributesWithCustomFilter(
123
            $filter,
124
            $limit,
125
            $current_cursor,
126
            $has_more,
127
            $node
128
        );
129
        $reset = false;
130
131
        if (count($children) === 0) {
132
            $id = 0;
133
        } else {
134
            $id = end($children)->getId();
135
        }
136
137
        if (null === $cursor) {
138
            $last = $this->getLastRecord();
139
            if (null === $last) {
140
                $delta_id = 0;
141
                $ts = new UTCDateTime();
142
            } else {
143
                $delta_id = $last['_id'];
144
                $ts = $last['timestamp'];
145
            }
146
147
            $reset = true;
148
            if (false === $has_more) {
149
                $cursor = base64_encode('delta|0|0|'.$delta_id.'|'.$ts);
150
            } else {
151
                $cursor = base64_encode('initial|'.$current_cursor.'|'.$id.'|'.$delta_id.'|'.$ts);
152
            }
153
        } else {
154
            if (false === $has_more) {
155
                $cursor = base64_encode('delta|0|0|'.$cursor[3].'|'.$cursor[4]);
156
            } else {
157
                $cursor = base64_encode('initial|'.$current_cursor.'|'.$id.'|'.$cursor[3].'|'.$cursor[4]);
158
            }
159
        }
160
161
        return [
162
            'reset' => $reset,
163
            'cursor' => $cursor,
164
            'has_more' => $has_more,
165
            'nodes' => $children,
166
        ];
167
    }
168
169
    /**
170
     * Get last delta event.
171
     */
172
    public function getLastRecord(?NodeInterface $node = null): ?array
173
    {
174
        $filter = $this->getDeltaFilter();
175
176
        if (null !== $node) {
177
            $filter = [
178
                '$and' => [
179
                    ['node' => $node->getId()],
180
                    $filter,
181
                ],
182
            ];
183
        }
184
185
        $cursor = $this->db->delta->find($filter, [
186
            'sort' => ['timestamp' => -1],
187
            'limit' => 1,
188
        ]);
189
190
        $last = $cursor->toArray();
191
192
        return array_shift($last);
193
    }
194
195
    /**
196
     * Get last cursor.
197
     */
198
    public function getLastCursor(?NodeInterface $node = null): string
199
    {
200
        $filter = $this->getDeltaFilter();
201
202
        if (null !== $node) {
203
            $filter = [
204
                '$and' => [
205
                    ['node' => $node->getId()],
206
                    $filter,
207
                ],
208
            ];
209
        }
210
211
        $count = $this->db->delta->count($filter);
0 ignored issues
show
Unused Code introduced by
$count 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...
212
        $last = $this->getLastRecord($node);
213
214
        if (null === $last) {
215
            return base64_encode('delta|0|0|0|'.new UTCDateTime());
216
        }
217
218
        return $cursor = base64_encode('delta|0|0|'.$last['_id'].'|'.$last['timestamp']);
0 ignored issues
show
Unused Code introduced by
$cursor 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...
219
    }
220
221
    /**
222
     * Get delta feed with changes and cursor.
223
     */
224
    public function getDeltaFeed(?string $cursor = null, int $limit = 250, ?NodeInterface $node = null): array
225
    {
226
        $query = $this->getDeltaQuery($cursor, $limit, $node);
227
        if (array_key_exists('nodes', $query)) {
228
            return $query;
229
        }
230
231
        $delta = [];
232
233
        foreach ($query['result'] as $log) {
234
            if (false === $query['has_more']) {
235
                $query['last_id'] = (string) $log['_id'];
236
                $query['last_ts'] = (string) $log['timestamp'];
237
            }
238
239
            try {
240
                $log_node = $this->fs->findNodeById($log['node'], null, NodeInterface::DELETED_EXCLUDE);
241
                if (null !== $node && !$node->isSubNode($log_node)) {
242
                    continue;
243
                }
244
245
                //include share children after a new reference was added, otherwise the children would be lost if the cursor is newer
246
                //than the create timestamp of the share reference
247
                if (('addCollectionReference' === $log['operation'] || 'undeleteCollectionReference' === $log['operation']) && $log_node->isReference()) {
248
                    $members = $this->fs->findNodesByFilter([
249
                        'shared' => $log_node->getShareId(),
250
                        'deleted' => false,
251
                    ]);
252
253
                    foreach ($members as $share_member) {
254
                        $delta[$share_member->getPath()] = $share_member;
255
                    }
256
                } elseif ('undeleteCollection' === $log['operation'] || 'undeleteCollectionShare' === $log['operation']) {
257
                    $log_node->doRecursiveAction(function ($sub_node) use (&$delta) {
258
                        $delta[$sub_node->getPath()] = $sub_node;
259
                    });
260
                }
261
262
                if (array_key_exists('previous', $log)) {
263
                    if (array_key_exists('parent', $log['previous'])) {
264
                        if ($log['previous']['parent'] === null) {
265
                            $previous_path = DIRECTORY_SEPARATOR.$log['name'];
266
                        } else {
267
                            $parent = $this->fs->findNodeById($log['previous']['parent']);
268
                            $previous_path = $parent->getPath().DIRECTORY_SEPARATOR.$log['name'];
269
                        }
270
                    } elseif (array_key_exists('name', $log['previous'])) {
271
                        if (null === $log['parent']) {
272
                            $previous_path = DIRECTORY_SEPARATOR.$log['previous']['name'];
273
                        } else {
274
                            $parent = $this->fs->findNodeById($log['parent']);
275
                            $previous_path = $parent->getPath().DIRECTORY_SEPARATOR.$log['previous']['name'];
276
                        }
277
                    } else {
278
                        $delta[$log_node->getPath()] = $log_node;
279
280
                        continue;
281
                    }
282
283
                    $deleted_node = [
284
                        'id' => (string) $log['node'],
285
                        'deleted' => true,
286
                        'created' => null,
287
                        'changed' => $log['timestamp'],
288
                        'path' => $previous_path,
289
                        'directory' => $log_node instanceof Collection,
290
                    ];
291
292
                    $delta[$previous_path] = $deleted_node;
293
                    $delta[$log_node->getPath()] = $log_node;
294
                } else {
295
                    $delta[$log_node->getPath()] = $log_node;
296
                }
297
            } catch (ForbiddenException $e) {
298
                //no delta entriy for a node where we do not have access to
299
            } catch (\Exception $e) {
300
                $deleted = $this->getDeletedNodeDelta($log);
301
302
                if ($deleted !== null) {
303
                    $delta[$deleted['path']] = $deleted;
304
                }
305
            }
306
        }
307
308
        $cursor = base64_encode('delta|'.$query['cursor'].'|0|'.$query['last_id'].'|'.$query['last_ts']);
309
310
        return [
311
            'reset' => false,
312
            'cursor' => $cursor,
313
            'has_more' => $query['has_more'],
314
            'nodes' => array_values($delta),
315
        ];
316
    }
317
318
    /**
319
     * Get event log.
320
     */
321
    public function getEventLog(int $limit = 100, int $skip = 0, ?NodeInterface $node = null, ?int &$total = null): Iterable
322
    {
323
        $filter = $this->getEventFilter();
324
325
        if (null !== $node) {
326
            $old = $filter;
327
            $filter = ['$and' => [[
328
                'node' => $node->getId(),
329
            ],
330
            $old, ]];
331
        }
332
333
        $total = $this->db->delta->count($filter);
334
        $result = $this->db->delta->find($filter, [
335
            'sort' => ['_id' => -1],
336
            'skip' => $skip,
337
            'limit' => $limit,
338
        ]);
339
340
        return $result;
341
    }
342
343
    /**
344
     * Get delta feed filter.
345
     */
346
    protected function buildDeltaFeedFilter(array $cursor, int $limit, ?NodeInterface $node): array
347
    {
348
        if (0 === $cursor[3]) {
349
            return $this->getDeltaFilter();
350
        }
351
352
        if (0 === $this->db->delta->count(['_id' => new ObjectId($cursor[3])])) {
353
            return $this->buildFeedFromCurrentState(null, $limit, $node);
354
        }
355
356
        $filter = $this->getDeltaFilter();
357
358
        return [
359
            '$and' => [
360
                ['timestamp' => ['$gte' => new UTCDateTime($cursor[4])]],
361
                ['_id' => ['$gt' => new ObjectId($cursor[3])]],
362
                $filter,
363
            ],
364
        ];
365
    }
366
367
    /**
368
     * Get delta feed with changes and cursor.
369
     */
370
    protected function getDeltaQuery(?string $cursor = null, int $limit = 250, ?NodeInterface $node = null): array
371
    {
372
        $cursor = $this->decodeCursor($cursor);
373
374
        if (null === $cursor || 'initial' === $cursor[0]) {
375
            return $this->buildFeedFromCurrentState($cursor, $limit, $node);
376
        }
377
378
        try {
379
            $filter = $this->buildDeltaFeedFilter($cursor, $limit, $node);
380
381
            $result = $this->db->delta->find($filter, [
382
                'skip' => (int) $cursor[1],
383
                'limit' => (int) $limit,
384
                'sort' => ['timestamp' => 1],
385
            ]);
386
387
            $left = $this->db->delta->count($filter, [
388
                'skip' => (int) $cursor[1],
389
                'sort' => ['timestamp' => 1],
390
            ]);
391
392
            $result = $result->toArray();
393
            $count = count($result);
394
        } catch (\Exception $e) {
395
            return $this->buildFeedFromCurrentState(null, $limit, $node);
396
        }
397
398
        $position = $cursor[1] += $limit;
399
        $has_more = ($left - $count) > 0;
400
        if (false === $has_more) {
401
            $position = 0;
402
        }
403
404
        return [
405
            'result' => $result,
406
            'has_more' => $has_more,
407
            'cursor' => $position,
408
            'last_id' => $cursor[3],
409
            'last_ts' => $cursor[4],
410
        ];
411
    }
412
413
    /**
414
     * Get delta event for a (forced) deleted node.
415
     */
416
    protected function getDeletedNodeDelta(array $event): ?array
417
    {
418
        try {
419
            if (null === $event['parent']) {
420
                $path = DIRECTORY_SEPARATOR.$event['name'];
421
            } else {
422
                $parent = $this->fs->findNodeById($event['parent']);
423
                $path = $parent->getPath().DIRECTORY_SEPARATOR.$event['name'];
424
            }
425
426
            $entry = [
427
                'id' => (string) $event['node'],
428
                'deleted' => true,
429
                'created' => null,
430
                'changed' => $event['timestamp'],
431
                'path' => $path,
432
            ];
433
434
            if ('deleteCollection' === substr($event['operation'], 0, 16)) {
435
                $entry['directory'] = true;
436
            } elseif ('deleteFile' === substr($event['operation'], 0, 10)) {
437
                $entry['directory'] = false;
438
            }
439
440
            return $entry;
441
        } catch (\Exception $e) {
442
            return null;
443
        }
444
    }
445
446
    /**
447
     * Parse client.
448
     */
449
    protected function parseClient(): bool
450
    {
451
        if (PHP_SAPI === 'cli') {
452
            $this->client = [
453
                'type' => 'system',
454
                'app' => 'system',
455
                'v' => null,
456
                'hostname' => null,
457
            ];
458
        } else {
459
            if (isset($_SERVER['HTTP_X_CLIENT'])) {
460
                $parts = explode('|', strip_tags($_SERVER['HTTP_X_CLIENT']));
461
                $count = count($parts);
462
463
                if (3 === $count) {
464
                    $this->client['v'] = $parts[1];
465
                    $this->client['hostname'] = $parts[2];
466
                } elseif (2 === $count) {
467
                    $this->client['v'] = $parts[1];
468
                }
469
470
                $this->client['app'] = $parts[0];
471
            }
472
473
            if (isset($_SERVER['PATH_INFO'])) {
474
                $parts = explode('/', $_SERVER['PATH_INFO']);
475
                if (count($parts) >= 2) {
476
                    $this->client['type'] = $parts[1];
477
                }
478
            }
479
        }
480
481
        return true;
482
    }
483
484
    /**
485
     * Get Event owner id.
486
     */
487
    protected function getEventOwner(NodeInterface $node): ObjectId
488
    {
489
        $user = $node->getFilesystem()->getUser();
490
        if (null === $user) {
491
            return $node->getOwner();
492
        }
493
494
        return $user->getId();
495
    }
496
497
    /**
498
     * Decode cursor.
499
     */
500
    protected function decodeCursor(?string $cursor): ?array
501
    {
502
        if (null === $cursor) {
503
            return null;
504
        }
505
506
        $cursor = base64_decode($cursor, true);
507
        if (false === $cursor) {
508
            return null;
509
        }
510
511
        $cursor = explode('|', $cursor);
512
        if (5 !== count($cursor)) {
513
            return null;
514
        }
515
        $cursor[1] = (int) $cursor[1];
516
517
        return $cursor;
518
    }
519
520
    /**
521
     * Get event filter for db query.
522
     */
523
    protected function getEventFilter(): array
524
    {
525
        $shares = [];
526
        $cursor = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_INCLUDE, [
527
            '$or' => [
528
                ['reference' => ['$exists' => true]],
529
                ['shared' => true],
530
            ],
531
        ]);
532
533
        foreach ($cursor as $share) {
534
            if ($this->acl->getAclPrivilege($share) != Acl::PRIVILEGE_WRITEPLUS) {
535
                $shares[] = $share->getRealId();
536
            }
537
        }
538
539
        return [
540
            '$or' => [
541
                ['share' => [
542
                    '$in' => $shares,
543
                ]], [
544
                    'owner' => $this->user->getId(),
545
                ],
546
            ],
547
        ];
548
    }
549
550
    /**
551
     * Get delta filter for db query.
552
     */
553
    protected function getDeltaFilter(): array
554
    {
555
        return [
556
            '$or' => [
557
                ['share' => [
558
                    '$in' => $this->user->getShares(),
559
                ]], [
560
                    'owner' => $this->user->getId(),
561
                ],
562
            ],
563
        ];
564
    }
565
566
    /**
567
     * Get children with custom filter.
568
     */
569
    protected function findNodeAttributesWithCustomFilter(
570
        ?array $filter = null,
571
        ?int $limit = null,
572
        ?int &$cursor = null,
573
        ?bool &$has_more = null,
574
        ?NodeInterface $parent = null
575
    ) {
576
        $delta = [];
577
        $has_more = false;
578
579
        $max = $limit;
580
        if ($parent === null) {
581
            $result = $this->db->storage->find($filter, [
582
                'skip' => $cursor,
583
                'limit' => ++$max,
584
            ]);
585
        } else {
586
            $query = [
587
                ['$match' => $filter],
588
                ['$match' => ['_id' => $parent->getId()]],
589
                ['$graphLookup' => [
590
                    'from' => 'storage',
591
                    'startWith' => '$pointer',
592
                    'connectFromField' => 'pointer',
593
                    'connectToField' => 'parent',
594
                    'as' => 'children',
595
                ]],
596
                ['$match' => ['_id' => $parent->getId()]],
597
                ['$unwind' => '$children'],
598
                ['$skip' => $cursor],
599
                ['$limit' => ++$max],
600
            ];
601
602
            $result = $this->db->storage->aggregate($query);
603
        }
604
605
        $requested = $cursor;
606
        foreach ($result as $key => $node) {
607
            try {
608
                if (isset($node['children'])) {
609
                    $node = $node['children'];
610
                }
611
612
                $node = $this->fs->initNode($node);
613
            } catch (\Exception $e) {
614
                continue;
615
            }
616
617
            if (count($delta) >= $limit) {
618
                if ($requested === null || $requested === 0) {
619
                    array_unshift($delta, $parent);
620
                }
621
622
                $has_more = true;
623
624
                return $delta;
625
            }
626
627
            $delta[$node->getPath()] = $node;
628
            ++$cursor;
629
        }
630
631
        if ($requested === null || $requested === 0) {
632
            array_unshift($delta, $parent);
633
        }
634
635
        return $delta;
636
    }
637
}
638