Completed
Push — master ( 574516...7ce0a4 )
by Raffael
08:17
created

Node::setName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * Balloon
6
 *
7
 * @author      Raffael Sahli <[email protected]>
8
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPLv3 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use \Sabre\DAV;
15
use Balloon\Exception;
16
use Balloon\Helper;
17
use Balloon\User;
18
use Balloon\Filesystem;
19
use \PHPZip\Zip\Stream\ZipStream;
20
use \MongoDB\BSON\ObjectId;
21
use \MongoDB\BSON\UTCDateTime;
22
use \MongoDB\Model\BSONDocument;
23
use \Normalizer;
24
25
abstract class Node implements INode, DAV\INode
26
{
27
    /**
28
     * name max lenght
29
     */
30
    const MAX_NAME_LENGTH = 255;
31
32
33
    /**
34
     * Unique id
35
     *
36
     * @var ObjectId
37
     */
38
    protected $_id;
39
    
40
41
    /**
42
     * Node name
43
     *
44
     * @var string
45
     */
46
    protected $name = '';
47
    
48
     
49
    /**
50
     * Owner
51
     *
52
     * @var ObjectId
53
     */
54
    protected $owner;
55
56
57
    /**
58
     * User
59
     *
60
     * @var \Balloon\User
61
     */
62
    protected $user;
63
   
64
65
    /**
66
     * Meta attributes
67
     *
68
     * @var array
69
     */
70
    protected $meta = [];
71
    
72
73
    /**
74
     * Parent collection
75
     *
76
     * @var ObjectId
77
     */
78
    protected $parent;
79
80
    
81
    /**
82
     * Is file deleted
83
     *
84
     * @var bool|UTCDateTime
85
     */
86
    protected $deleted = false;
87
88
    
89
    /**
90
     * Is collection
91
     *
92
     * @var bool
93
     */
94
    protected $directory = false;
95
96
97
    /**
98
     * Is shared?
99
     *
100
     * @var bool
101
     */
102
    protected $shared = false;
103
104
    
105
    /**
106
     * Destory at a certain time
107
     *
108
     * @var UTCDateTime
109
     */
110
    protected $destroy;
111
112
113
    /**
114
     * Changed timestamp
115
     *
116
     * @var UTCDateTime
117
     */
118
    protected $changed;
119
120
    
121
    /**
122
     * Created timestamp
123
     *
124
     * @var UTCDateTime
125
     */
126
    protected $created;
127
128
129
    /**
130
     * Point to antother node (Means this node is reference to $reference)
131
     *
132
     * @var ObjectId
133
     */
134
    protected $reference;
135
136
137
    /**
138
     * Share link options
139
     *
140
     * @var bool|array
141
     */
142
    protected $sharelink = false;
143
144
145
    /**
146
     * Raw attributes before any processing or modifications
147
     *
148
     * @var array
149
     */
150
    protected $raw_attributes;
151
152
    
153
    /**
154
     * Readonly flag
155
     *
156
     * @var bool
157
     */
158
    protected $readonly = false;
159
160
    
161
    /**
162
     * Filesystem
163
     *
164
     * @var Filesystem
165
     */
166
    protected $_fs;
167
168
169
    /**
170
     * Database
171
     *
172
     * @var \MongoDB\Database
173
     */
174
    protected $_db;
175
176
177
    /**
178
     * User
179
     *
180
     * @var User
181
     */
182
    protected $_user;
183
184
    
185
    /**
186
     * Logger
187
     *
188
     * @var Logger
189
     */
190
    protected $_logger;
191
192
193
    /**
194
     * Plugin
195
     *
196
     * @var Plugin
197
     */
198
    protected $_pluginmgr;
199
    
200
201
    /**
202
     * Queue
203
     *
204
     * @var Queue
205
     */
206
    protected $_queuemgr;
207
208
    
209
    /**
210
     * Config
211
     *
212
     * @var Config
213
     */
214
    protected $_config;
215
216
217
    /**
218
     * Initialize
219
     *
220
     * @param  BSONDocument $node
221
     * @param  Filesystem $fs
222
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
223
     */
224
    public function __construct(?BSONDocument $node, Filesystem $fs)
225
    {
226
        $this->_fs         = $fs;
227
        $this->_db         = $fs->getDatabase();
228
        $this->_user       = $fs->getUser();
229
        $this->_logger     = $fs->getLogger();
0 ignored issues
show
Documentation Bug introduced by
It seems like $fs->getLogger() of type object<Psr\Log\LoggerInterface> is incompatible with the declared type object<Balloon\Filesystem\Node\Logger> of property $_logger.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
230
        $this->_pluginmgr  = $fs->getPlugin();
0 ignored issues
show
Documentation Bug introduced by
It seems like $fs->getPlugin() of type object<Balloon\Plugin> is incompatible with the declared type object<Balloon\Filesystem\Node\Plugin> of property $_pluginmgr.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
231
        $this->_queuemgr   = $fs->getQueue();
0 ignored issues
show
Documentation Bug introduced by
It seems like $fs->getQueue() of type object<Balloon\Queue> is incompatible with the declared type object<Balloon\Filesystem\Node\Queue> of property $_queuemgr.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
232
        $this->_config     = $fs->getConfig();
0 ignored issues
show
Documentation Bug introduced by
It seems like $fs->getConfig() of type object<Balloon\Config> is incompatible with the declared type object<Balloon\Filesystem\Node\Config> of property $_config.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
233
234
        if ($node !== null) {
235
            $node = Helper::convertBSONDocToPhp($node);
236
            foreach ($node as $attr => $value) {
237
                $this->{$attr} = $value;
238
            }
239
240
            $this->raw_attributes = $node;
0 ignored issues
show
Documentation Bug introduced by
It seems like $node can also be of type object<MongoDB\Model\BSONArray> or object<MongoDB\Model\BSONDocument>. However, the property $raw_attributes is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
241
        }
242
    }
243
244
245
    /**
246
     * Convert to filename
247
     *
248
     * @return string
249
     */
250
    public function __toString()
251
    {
252
        return $this->name;
253
    }
254
255
256
    /**
257
     * Get filesystem
258
     *
259
     * @return Filesystem
260
     */
261
    public function getFilesystem(): Filesystem
262
    {
263
        return $this->_fs;
264
    }
265
266
267
    /**
268
     * Get property
269
     *
270
     * @return mixed
271
     */
272
    public function __call(string $attribute, array $params=[])
273
    {
274
        $prefix = 'get';
0 ignored issues
show
Unused Code introduced by
$prefix 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...
275
        $attr = strtolower(substr($attribute, 3));
276
        if (property_exists($this, $attr)) {
277
            return $this->{$attr};
278
        } else {
279
            throw new Exception('method '.$attribute.' does not exists');
280
        }
281
    }
282
283
284
    /**
285
     * Check if $node is a sub node of any parent nodes of this node
286
     *
287
     * @param   INode $node
288
     * @return  bool
289
     */
290
    public function isSubNode(INode $node): bool
291
    {
292
        if ($node->getId() == $this->_id) {
293
            return true;
294
        }
295
296
        foreach ($node->getParents() as $node) {
297
            if ($node->getId() == $this->_id) {
298
                return true;
299
            }
300
        }
301
        
302
        if ($this->isRoot()) {
303
            return true;
304
        }
305
306
        return false;
307
    }
308
309
    
310
    /**
311
     * Move node
312
     *
313
     * @param  Collection $parent
314
     * @param  int $conflict
315
     * @return INode
316
     */
317
    public function setParent(Collection $parent, int $conflict=INode::CONFLICT_NOACTION): INode
318
    {
319
        if ($this->parent === $parent->getId()) {
320
            throw new Exception\Conflict('source node '.$this->name.' is already in the requested parent folder',
321
                Exception\Conflict::ALREADY_THERE
322
            );
323
        } elseif ($this->isSubNode($parent)) {
324
            throw new Exception\Conflict('node called '.$this->name.' can not be moved into itself',
325
                Exception\Conflict::CANT_BE_CHILD_OF_ITSELF
326
            );
327
        } elseif (!$this->isAllowed('w') && !$this->isReference()) {
328
            throw new Exception\Forbidden('not allowed to move node '.$this->name,
329
                Exception\Forbidden::NOT_ALLOWED_TO_MOVE
330
            );
331
        }
332
        
333
        $exists = $parent->childExists($this->name);
334
        if ($exists === true && $conflict === INode::CONFLICT_NOACTION) {
335
            throw new Exception\Conflict('a node called '.$this->name.' does already exists in this collection',
336
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
337
            );
338
        } elseif ($this->isShared() && $this instanceof Collection && $parent->isShared()) {
339
            throw new Exception\Conflict('a shared folder can not be a child of a shared folder too',
340
                Exception\Conflict::SHARED_NODE_CANT_BE_CHILD_OF_SHARE
341
            );
342
        } elseif ($parent->isDeleted()) {
343
            throw new Exception\Conflict('cannot move node into a deleted collction',
344
                Exception\Conflict::DELETED_PARENT
345
            );
346
        }
347
348
        if ($exists === true && $conflict == INode::CONFLICT_RENAME) {
349
            $this->setName($this->_getDuplicateName());
350
            $this->raw_attributes['name'] = $this->name;
351
        }
352
        
353
        if ($this instanceof Collection) {
354
            $this->getChildrenRecursive($this->getRealId(), $shares);
0 ignored issues
show
Bug introduced by
The variable $shares does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Documentation Bug introduced by
The method getRealId does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method getChildrenRecursive does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
355
356
            if (!empty($shares) && $parent->isShared()) {
357
                throw new Exception\Conflict('folder contains a shared folder',
358
                    Exception\Conflict::NODE_CONTAINS_SHARED_NODE
359
                );
360
            }
361
        }
362
363
        if ($parent->isSpecial() && $this->shared != $parent->getShareId() || !$parent->isSpecial() && $this->isShareMember()) {
364
            $new = $this->copyTo($parent, $conflict);
365
            $this->delete();
366
            return $new;
367
        }
368
369
        if ($exists === true && $conflict == INode::CONFLICT_MERGE) {
370
            $new = $this->copyTo($parent, $conflict);
371
            $this->delete(true/*, false, false*/);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
372
            return $new;
373
        }
374
375
        $this->parent = $parent->getRealId();
376
        $this->owner  = $this->_user->getId();
377
378
        $this->save(['parent', 'shared', 'owner']);
379
        return $this;
380
    }
381
382
383
    /**
384
     * Copy node
385
     *
386
     * @param   Collection $parent
387
     * @param   int $conflict
388
     * @param   string $recursion
389
     * @param   bool $recursion_first
390
     * @return  INode
391
     */
392
    abstract public function copyTo(Collection $parent, int $conflict=INode::CONFLICT_NOACTION, ?string $recursion=null, bool $recursion_first=true): INode;
393
394
395
    /**
396
     * Get share id
397
     *
398
     * @param   bool $reference
399
     * @return  ObjectId
400
     */
401
    public function getShareId(bool $reference=false): ?ObjectId
402
    {
403
        if ($this->isReference() && $reference === true) {
404
            return $this->_id;
405
        } elseif ($this->isShareMember() && $reference === true) {
406
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type documented by Balloon\Filesystem\Node\Node::getShareId of type MongoDB\BSON\ObjectId|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
407
        } elseif ($this->isShared() && $this->isReference()) {
408
            return $this->reference;
409
        } elseif ($this->isShared()) {
410
            return $this->_id;
411
        } elseif ($this->isShareMember()) {
412
            return $this->shared;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->shared; (boolean) is incompatible with the return type documented by Balloon\Filesystem\Node\Node::getShareId of type MongoDB\BSON\ObjectId|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
413
        } else {
414
            return null;
415
        }
416
    }
417
418
419
    /**
420
     * Check node
421
     *
422
     * @return void
423
     */
424
    protected function _verifyAccess()
425
    {
426
        if (!$this->isAllowed('r')) {
427
            throw new Exception\Forbidden('not allowed to access node',
428
                Exception\Forbidden::NOT_ALLOWED_TO_ACCESS
429
            );
430
        }
431
432
        if ($this->destroy instanceof UTCDateTime && $this->destroy->toDateTime()->format('U') <= time()) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
433
            $this->_logger->info('node ['.$this->_id.'] is not accessible anmyore, destroy node cause of expired destroy flag', [
434
                'category' => get_class($this)
435
            ]);
436
            
437
            $this->delete(true);
438
            throw new Exception\Conflict('node is not available anymore');
439
        }
440
    }
441
442
443
    /**
444
     * Get share node
445
     *
446
     * @param   bool $reference
0 ignored issues
show
Bug introduced by
There is no parameter named $reference. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
447
     * @return  Collection
448
     */
449
    public function getShareNode(): ?Collection
450
    {
451
        if ($this->isSpecial()) {
452
            return $this->_fs->findNodeWithId($this->getShareId(true));
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId(true) can be null; however, findNodeWithId() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
453
        } else {
454
            return null;
455
        }
456
    }
457
458
459
    /**
460
     * Is node marked as readonly?
461
     *
462
     * @return bool
463
     */
464
    public function isReadonly(): bool
465
    {
466
        return $this->readonly;
467
    }
468
469
470
    /**
471
     * Request is from node owner?
472
     *
473
     * @return bool
474
     */
475
    public function isOwnerRequest(): bool
476
    {
477
        return ($this->_user !== null && $this->owner == $this->_user->getId());
478
    }
479
480
481
    /**
482
     * Check if node is kind of special
483
     *
484
     * @return bool
485
     */
486
    public function isSpecial(): bool
487
    {
488
        if ($this->isShared()) {
489
            return true;
490
        } elseif ($this->isReference()) {
491
            return true;
492
        } elseif ($this->isShareMember() /*&& $this instanceof Collection*/) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
493
            return true;
494
        } else {
495
            return false;
496
        }
497
    }
498
499
500
    /**
501
     * Check if node is a sub node of a share
502
     *
503
     * @return bool
504
     */
505
    public function isShareMember(): bool
506
    {
507
        return ($this->shared instanceof ObjectId && !$this->isReference());
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
508
    }
509
510
511
    /**
512
     * Is share
513
     *
514
     * @return bool
515
     */
516
    public function isShare(): bool
517
    {
518
        return ($this->shared === true && !$this->isReference());
519
    }
520
521
522
    /**
523
     * Is share (Reference or master share)
524
     *
525
     * @return bool
526
     */
527
    public function isShared(): bool
528
    {
529
        if ($this->shared === true) {
530
            return true;
531
        } else {
532
            return false;
533
        }
534
    }
535
536
537
    /**
538
     * Set the name
539
     *
540
     * @param  string $name
541
     * @return bool
542
     */
543
    public function setName($name): bool
544
    {
545
        $name = $this->checkName($name);
546
547
        if ($this->getParent()->childExists($name)) {
548
            throw new Exception\Conflict('a node called '.$name.' does already exists in this collection',
549
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
550
            );
551
        }
552
        
553
        $this->name = $name;
554
        return $this->save('name');
555
    }
556
    
557
558
    /**
559
     * Check name
560
     *
561
     * @param   string $name
562
     * @return  string
563
     */
564
    public function checkName(string $name): string
565
    {
566
        if (preg_match('/([\\\<\>\:\"\/\|\*\?])|(^$)|(^\.$)|(^\..$)/', $name)) {
567
            throw new Exception\InvalidArgument('name contains invalid characters');
568
        } elseif (strlen($name) > self::MAX_NAME_LENGTH) {
569
            throw new Exception\InvalidArgument('name is longer than '.self::MAX_NAME_LENGTH.' characters');
570
        }
571
572
        if (!Normalizer::isNormalized($name)) {
573
            $name = Normalizer::normalize($name);
574
        }
575
 
576
        return $name;
577
    }
578
579
580
    /**
581
     * Get the name
582
     *
583
     * @return string
584
     */
585
    public function getName(): string
586
    {
587
        return $this->name;
588
    }
589
590
591
    /**
592
     * Check acl
593
     *
594
     * @param   string $privilege
595
     * @return  bool
596
     */
597
    public function isAllowed(string $privilege='r'): bool
598
    {
599
        $acl   = null;
0 ignored issues
show
Unused Code introduced by
$acl 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...
600
        $share = null;
0 ignored issues
show
Unused Code introduced by
$share 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...
601
602
        $this->_logger->debug('check acl for ['.$this->_id.'] with privilege ['.$privilege.']', [
603
            'category' => get_class($this),
604
        ]);
605
 
606
        if ($this->_user === null) {
607
            $this->_logger->debug('system acl call, grant full access', [
608
                'category' => get_class($this),
609
            ]);
610
611
            return true;
612
        }
613
614
        /* TODO: writeonly does not work with this part:
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
615
        if($this->_user->getId() == $this->owner) {
616
            $this->_logger->debug('owner request detected for ['.$this->_id.'], grant full access', [
617
                'category' => get_class($this),
618
            ]);
619
620
            return true;
621
        }
622
        */
623
624
        $priv = $this->getAclPrivilege();
625
626
        $result = false;
627
628
        if ($priv === 'w+' && $this->isOwnerRequest()) {
629
            $result = true;
630
        } elseif ($this->isShared() || $this->isReference()) {
631
            if ($privilege === 'r' && ($priv === 'r' || $priv === 'w' || $priv === 'rw')) {
632
                $result = true;
633
            } elseif ($privilege === 'w' && ($priv === 'w' || $priv === 'rw')) {
634
                $result = true;
635
            }
636
        } else {
637
            if ($privilege === 'r' && ($priv === 'r' || $priv === 'rw')) {
638
                $result = true;
639
            } elseif ($privilege === 'w' && ($priv === 'w' || $priv === 'rw')) {
640
                $result = true;
641
            }
642
        }
643
        
644
        $this->_logger->debug('check acl for node ['.$this->_id.'], requested privilege ['.$privilege.']', [
645
            'category' => get_class($this),
646
            'params'   => ['privileges' => $priv],
647
        ]);
648
        
649
        return $result;
650
    }
651
652
    
653
    /**
654
     * Get privilege
655
     *
656
     * rw - READ/WRITE
657
     * r  - READ(only)
658
     * w  - WRITE(only)
659
     * d  - DENY
660
     *
661
     * @return string
662
     */
663
    public function getAclPrivilege()
664
    {
665
        $result = false;
666
        $acl    = [];
667
        $user   = $this->_user;
668
669
        if ($this->isShareMember()) {
670
            try {
671
                $share = $this->_fs->findRawNode($this->shared);
0 ignored issues
show
Documentation introduced by
$this->shared is of type boolean, but the function expects a object<MongoDB\BSON\ObjectId>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
672
            } catch (\Exception $e) {
673
                $this->_logger->error('could not found share node ['.$this->shared.'] for share child node ['.$this->_id.'], dead reference?', [
674
                    'category' => get_class($this),
675
                    'exception' => $e,
676
                ]);
677
                
678
                return 'd';
679
            }
680
           
681
            if ((string)$share['owner'] === (string)$user->getId()) {
682
                return 'rw';
683
            }
684
685
            $acl = $share['acl'];
686
        } elseif ($this->isReference() && $this->isOwnerRequest()) {
687
            try {
688
                $share = $this->_fs->findRawNode($this->reference);
689
            } catch (\Exception $e) {
690
                $this->_logger->error('could not found share node ['.$this->shared.'] for reference ['.$this->_id.'], dead reference?', [
691
                    'category'  => get_class($this),
692
                    'exception' => $e,
693
                ]);
694
695
                $this->_forceDelete();
696
                return 'd';
697
            }
698
            
699
            if ($share['deleted'] instanceof UTCDateTime || $share['shared'] !== true) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
700
                $this->_logger->error('share node ['.$share['_id'].'] has been deleted, dead reference?', [
701
                    'category' => get_class($this),
702
                ]);
703
704
                $this->_forceDelete();
705
                return 'd';
706
            }
707
            
708
            $acl = $share['acl'];
709
        } elseif (!$this->isOwnerRequest()) {
710
            $this->_logger->warning("user [".$this->_user->getUsername()."] not allowed to access non owned node [".$this->_id."]", [
711
                'category' => get_class($this),
712
            ]);
713
714
            return 'd';
715
        } elseif ($this->isOwnerRequest()) {
716
            return 'rw';
717
        }
718
        
719
        if (!is_array($acl)) {
720
            return 'd';
721
        }
722
 
723
        if (array_key_exists('user', $acl)) {
724
            foreach ($acl['user'] as $rule) {
725
                if (array_key_exists('user', $rule) && $rule['user'] == $user->getUsername() && array_key_exists('priv', $rule)) {
726
                    $result = $rule['priv'];
727
                } elseif (array_key_exists('ldapdn', $rule) && $rule['ldapdn'] == $user->getLdapDn() && array_key_exists('priv', $rule)) {
728
                    $result = $rule['priv'];
729
                }
730
731
                if ($result == 'd') {
732
                    return $result;
733
                }
734
            }
735
        }
736
                
737
        if (array_key_exists('group', $acl)) {
738
            $groups = $user->getGroups();
739
            
740
            foreach ($acl['group'] as $rule) {
741
                if (array_key_exists('group', $rule) && in_array($rule['group'], $groups) && array_key_exists('priv', $rule)) {
742
                    $group_result = $rule['priv'];
743
                    
744
                    if ($group_result == 'd') {
745
                        return $group_result;
746
                    } elseif ($result === false) {
747
                        $result = $group_result;
748
                    } elseif ($result == 'r' && ($group_result == 'w' || $group_result == 'rw')) {
749
                        $result = $group_result;
750
                    } elseif ($group_result == 'rw') {
751
                        $result = $group_result;
752
                    }
753
                }
754
            }
755
        }
756
        
757
        if ($result === false) {
758
            return 'd';
759
        } else {
760
            return $result;
761
        }
762
    }
763
    
764
765
    /**
766
     * Get Attribute helper
767
     *
768
     * @param  array|string $attribute
769
     * @return array|string
770
     */
771
    protected function _getAttribute($attribute)
772
    {
773
        $requested = $attribute;
774
        $attribute = (array)$attribute;
775
        $metacheck = $attribute;
776
        $meta      = [];
777
        $clean     = [];
778
779
        foreach ($metacheck as $key => $attr) {
780
            if (substr($attr, 0, 5) == 'meta.') {
781
                $meta[] = substr($attr, 5);
782
            } else {
783
                $clean[] = $attr;
784
            }
785
        }
786
        
787
        if (!empty($meta)) {
788
            $clean[] = 'meta';
789
        }
790
        
791
        $attribute  = $clean;
792
793
        try {
794
            $sharenode  = $this->getShareNode();
795
        } catch (\Exception $e) {
796
            $sharenode  = null;
797
        }
798
799
        $build = [];
800
801
        foreach ($attribute as $key => $attr) {
802
            switch ($attr) {
803
                case 'id':
804
                    $build['id'] = (string)$this->_id;
805
                break;
806
                
807
                case 'name':
808
                case 'mime':
809
                    $build[$attr] = (string)$this->{$attr};
810
                break;
811
                
812
                case 'parent':
813
                    try {
814
                        $parent = $this->getParent();
815
                        if ($parent === null || $parent->getId() === null) {
816
                            $build[$attr] = null;
817
                        } else {
818
                            $build[$attr] = (string)$parent->getId();
819
                        }
820
                    } catch (\Exception $e) {
821
                        $build[$attr] = null;
822
                    }
823
                break;
824
825
                case 'meta':
826
                    $build['meta'] = (object)$this->getMetaAttribute($meta);
827
                break;
828
            
829
                case 'size':
830
                    $build['size'] = $this->getSize();
0 ignored issues
show
Documentation Bug introduced by
The method getSize does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
831
                break;
832
            
833
                case 'sharelink':
834
                    $build['sharelink'] = $this->isShareLink();
835
                break;
836
                
837
                case 'deleted':
838
                case 'changed':
839
                case 'created':
840
                case 'destroy':
841
                    if ($this->{$attr} instanceof UTCDateTime) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
842
                        $build[$attr] = Helper::DateTimeToUnix($this->{$attr});
843
                    } else {
844
                        $build[$attr] = $this->{$attr};
845
                    }
846
                break;
847
848
                case 'readonly':
849
                case 'directory':
850
                    $build[$attr] = $this->{$attr};
851
                break;
852
853
                case 'path':
854
                    try {
855
                        $build['path'] = $this->getPath();
856
                    } catch (\Balloon\Exception\NotFound $e) {
857
                        $build['path'] = null;
858
                    }
859
                break;
860
861
                case 'shared':
862
                    if ($this->directory === true) {
863
                        $build['shared'] = $this->isShared();
864
                    }
865
                break;
866
                
867
                case 'filtered':
868
                    if ($this->directory === true) {
869
                        $build['filtered'] = $this->isCustomFilter();
0 ignored issues
show
Documentation Bug introduced by
The method isCustomFilter does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
870
                    }
871
                break;
872
                
873
                case 'reference':
874
                    if ($this->directory === true) {
875
                        $build['reference'] = $this->isReference();
876
                    }
877
                break;
878
879
                case 'share':
880
                    if ($this->isSpecial() && $sharenode !== null) {
881
                        $build['share'] = $sharenode->getName();
882
                    } else {
883
                        $build['share'] = false;
884
                    }
885
                break;
886
 
887
                case 'access':
888
                    if ($this->isSpecial() && $sharenode !== null) {
889
                        $build['access'] = $sharenode->getAclPrivilege();
890
                    }
891
                break;
892
 
893
                case 'shareowner':
894
                    if ($this->isSpecial() && $sharenode !== null) {
895
                        $build['shareowner'] = (new User($this->_fs->findRawNode($this->getShareId())['owner'],
0 ignored issues
show
Bug introduced by
It seems like $this->getShareId() can be null; however, findRawNode() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
896
                          $this->_logger, $this->_fs)
897
                        )->getUsername();
898
                    }
899
                break;
900
            }
901
        }
902
            
903
        if (is_string($requested)) {
904
            if (array_key_exists($requested, $build)) {
905
                return $build[$requested];
906
            } else {
907
                return null;
908
            }
909
        }
910
911
        return $build;
912
    }
913
914
915
    /**
916
     * Duplicate name with a uniqid within name
917
     *
918
     * @param   string $name
919
     * @return  string
920
     */
921
    protected function _getDuplicateName(?string $name=null): string
922
    {
923
        if ($name === null) {
924
            $name = $this->name;
925
        }
926
927
        if ($this instanceof Collection) {
928
            return $name.' ('.substr(uniqid('', true), -4).')';
929
        } else {
930
            $ext  = substr(strrchr($name, '.'), 1);
931
932
            if ($ext === false) {
933
                return $name.' ('.substr(uniqid('', true), -4).')';
934
            } else {
935
                $name = substr($name, 0, -(strlen($ext) + 1));
936
                return $name.' ('.substr(uniqid('', true), -4).')'.'.'.$ext;
937
            }
938
        }
939
    }
940
941
942
    /**
943
     * Undelete
944
     *
945
     * @param   int $conflict
946
     * @param   string $recursion
947
     * @param   bool $recursion_first
948
     * @return  bool
949
     */
950
    public function undelete(int $conflict=INode::CONFLICT_NOACTION, ?string $recursion=null, bool $recursion_first=true): bool
0 ignored issues
show
Unused Code introduced by
The parameter $recursion_first is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
951
    {
952
        if (!$this->isAllowed('w')) {
953
            throw new Exception\Forbidden('not allowed to restore node '.$this->name,
954
                Exception\Forbidden::NOT_ALLOWED_TO_UNDELETE
955
            );
956
        } elseif (!$this->isDeleted()) {
957
            throw new Exception\Conflict('node is not deleted, skip restore',
958
                Exception\Conflict::NOT_DELETED
959
            );
960
        }
961
        
962
        $parent = $this->getParent();
963
        if ($parent->isDeleted()) {
964
            throw new Exception\Conflict('could not restore node '.$this->name.' into a deleted parent',
965
                Exception\Conflict::DELETED_PARENT
966
            );
967
        }
968
969
        if ($parent->childExists($this->name)) {
970
            if ($conflict == INode::CONFLICT_MERGE) {
971
                $this->copyTo($parent, $conflict);
0 ignored issues
show
Bug introduced by
It seems like $parent defined by $this->getParent() on line 962 can be null; however, Balloon\Filesystem\Node\Node::copyTo() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
972
                $this->delete(true);
973
            } elseif ($conflict === INode::CONFLICT_RENAME) {
974
                $this->setName($this->_getDuplicateName());
975
                $this->raw_attributes['name'] = $this->name;
976
            } else {
977
                throw new Exception\Conflict('a node called '.$this->name.' does already exists in this collection',
978
                    Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
979
                );
980
            }
981
        }
982
983
        if ($recursion === null) {
984
            $recursion_first = true;
985
            $recursion = uniqid();
986
        } else {
987
            $recursion_first = false;
988
        }
989
990
        $this->deleted  = false;
991
        
992
        if ($this instanceof File) {
993
            $current = $this->version;
0 ignored issues
show
Bug introduced by
The property version does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Unused Code introduced by
$current 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...
994
            $new = $this->increaseVersion();
0 ignored issues
show
Documentation Bug introduced by
The method increaseVersion does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
995
996
            $this->history[] = [
0 ignored issues
show
Bug introduced by
The property history does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
997
                'version' => $new,
998
                'changed' => $this->changed,
999
                'user'    => $this->owner,
1000
                'type'    => File::HISTORY_UNDELETE,
1001
                'file'    => $this->file,
0 ignored issues
show
Bug introduced by
The property file does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1002
                'size'    => $this->size,
0 ignored issues
show
Bug introduced by
The property size does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1003
                'mime'    => $this->mime,
0 ignored issues
show
Bug introduced by
The property mime does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1004
            ];
1005
            
1006
            return $this->save([
1007
                'name',
1008
                'deleted',
1009
                'history',
1010
                'version'
1011
            ], [], $recursion, $recursion_first);
1012
        } else {
1013
            $this->save([
1014
                'name',
1015
                'deleted',
1016
            ], [], $recursion, $recursion_first);
1017
            
1018
            return $this->doRecursiveAction('undelete', [
0 ignored issues
show
Documentation Bug introduced by
The method doRecursiveAction does not exist on object<Balloon\Filesystem\Node\Node>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
1019
                    'conflict'        => $conflict,
1020
                    'recursion'       => $recursion,
1021
                    'recursion_first' => false
1022
                ],
1023
                INode::DELETED_ONLY
1024
            );
1025
        }
1026
1027
        return true;
0 ignored issues
show
Unused Code introduced by
return true; 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...
1028
    }
1029
1030
1031
    /**
1032
     * Is node deleted?
1033
     *
1034
     * @return bool
1035
     */
1036
    public function isDeleted(): bool
1037
    {
1038
        return $this->deleted instanceof UTCDateTime;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1039
    }
1040
1041
1042
    /**
1043
     * Share link
1044
     *
1045
     * @param   array $options
1046
     * @return  bool
1047
     */
1048
    public function shareLink(array $options): bool
1049
    {
1050
        $valid = [
1051
            'shared',
1052
            'token',
1053
            'password',
1054
            'expiration',
1055
        ];
1056
1057
        $set = [];
1058
        foreach ($options as $option => $v) {
1059
            if (!in_array($option, $valid, true)) {
1060
                throw new Exception\InvalidArgument('share option '.$option.' is not valid');
1061
            } else {
1062
                $set[$option] = $v;
1063
            }
1064
        }
1065
1066
        if (!array_key_exists('token', $set)) {
1067
            $set['token'] = uniqid((string)$this->_id, true);
1068
        }
1069
1070
        if (array_key_exists('expiration', $set)) {
1071
            if (empty($set['expiration'])) {
1072
                unset($set['expiration']);
1073
            } else {
1074
                $set['expiration'] = (int)$set['expiration'];
1075
            }
1076
        }
1077
        
1078
        if (array_key_exists('password', $set)) {
1079
            if (empty($set['password'])) {
1080
                unset($set['password']);
1081
            } else {
1082
                $set['password'] = hash('sha256', $set['password']);
1083
            }
1084
        }
1085
        
1086
        $share = false;
1087
        if (!array_key_exists('shared', $set)) {
1088
            if (!is_array($this->sharelink)) {
1089
                $share = true;
1090
            }
1091
        } else {
1092
            if ($set['shared'] === 'true' || $set['shared'] === true) {
1093
                $share = true;
1094
            }
1095
1096
            unset($set['shared']);
1097
        }
1098
        
1099
        if ($share === true) {
1100
            $this->sharelink = $set;
1101
            return $this->save(['sharelink']);
1102
        } else {
1103
            $this->sharelink = null;
1104
            return $this->save([], ['sharelink']);
1105
        }
1106
    }
1107
1108
1109
    /**
1110
     * Get share options
1111
     *
1112
     * @return bool|array
1113
     */
1114
    public function getShareLink()
1115
    {
1116
        if (!$this->isShareLink()) {
1117
            return false;
1118
        } else {
1119
            return $this->sharelink;
1120
        }
1121
    }
1122
1123
1124
    /**
1125
     * Get last modified timestamp
1126
     *
1127
     * @return int
1128
     */
1129
    public function getLastModified(): int
1130
    {
1131
        if ($this->changed instanceof UTCDateTime) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1132
            return (int)$this->changed->toDateTime()->format('U');
1133
        } else {
1134
            return 0;
1135
        }
1136
    }
1137
1138
1139
    /**
1140
     * Get unique id
1141
     *
1142
     * @return ObjectId|string
1143
     */
1144
    public function getId(bool $string=false)
1145
    {
1146
        if ($string === true) {
1147
            return (string)$this->_id;
1148
        } else {
1149
            return $this->_id;
1150
        }
1151
    }
1152
1153
1154
    /**
1155
     * Get parent
1156
     *
1157
     * @return Collection
1158
     */
1159
    public function getParent(): ?Collection
1160
    {
1161
        try {
1162
            if ($this->isRoot()) {
1163
                return null;
1164
            } elseif ($this->isInRoot()) {
1165
                return $this->_fs->getRoot();
1166
            } else {
1167
                $parent = $this->_fs->findNodeWithId($this->parent);
1168
                if ($parent->isShare() && !$parent->isOwnerRequest() && $this->_user !== null) {
1169
                    $node = $this->_db->storage->findOne([
1170
                        'owner' => $this->_user->getId(),
1171
                        'shared' => true,
1172
                        'reference' => $this->parent,
1173
                    ]);
1174
                    
1175
                    return new Collection($node, $this->_fs);
1176
                } else {
1177
                    return $parent;
1178
                }
1179
            }
1180
        } catch (Exception\NotFound $e) {
1181
            throw new Exception\NotFound('parent node '.$this->parent.' could not be found',
1182
                Exception\NotFound::PARENT_NOT_FOUND
1183
            );
1184
        }
1185
    }
1186
1187
1188
    /**
1189
     * Get parents
1190
     *
1191
     * @param   array $parents
1192
     * @return  array
1193
     */
1194
    public function getParents(?INode $node=null, array $parents=[]): array
1195
    {
1196
        if ($node === null) {
1197
            $node = $this;
1198
        }
1199
1200
        if ($node->isInRoot()) {
1201
            return $parents;
1202
        } else {
1203
            $parent = $node->getParent();
1204
            $parents[] = $parent;
1205
            return $node->getParents($parent, $parents);
1206
        }
1207
1208
        return $parents;
0 ignored issues
show
Unused Code introduced by
return $parents; 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...
1209
    }
1210
1211
1212
    /**
1213
     * Check if the node is a shared link
1214
     *
1215
     * @return bool
1216
     */
1217
    public function isShareLink(): bool
1218
    {
1219
        return is_array($this->sharelink) && $this->sharelink !== false;
1220
    }
1221
1222
    
1223
    /**
1224
     * Download
1225
     *
1226
     * @return void
1227
     */
1228
    abstract public function get();
1229
1230
1231
    /**
1232
     * Get as zip
1233
     *
1234
     * @return void
1235
     */
1236
    public function getZip(): void
1237
    {
1238
        $temp = $this->_config->dir->temp.DIRECTORY_SEPARATOR.'zip';
1239
        if (!file_exists($temp)) {
1240
            mkdir($temp, 0700, true);
1241
        }
1242
1243
        ZipStream::$temp = $temp;
1244
        $archive = new ZipStream($this->name.".zip", "application/zip", $this->name.".zip");
1245
        $this->zip($archive, false);
1246
        $archive->finalize();
1247
        exit();
1248
    }
1249
1250
1251
    /**
1252
     * Create zip
1253
     *
1254
     * @param   ZipStream $archive
1255
     * @param   bool $self true means that the zip represents the collection itself instead a child of the zip
1256
     * @param   INode $parent
1257
     * @param   string $path
1258
     * @param   int $depth
1259
     * @return  bool
1260
     */
1261
    public function zip(ZipStream $archive, bool $self=true, ?INode $parent=null, string $path='', int $depth=0): bool
1262
    {
1263
        if ($parent === null) {
1264
            $parent = $this;
1265
        }
1266
1267
        if ($parent instanceof Collection) {
1268
            $children = $parent->getChildNodes();
1269
1270
            if ($self === true && $depth === 0) {
1271
                $path = $parent->getName();
1272
                $archive->addDirectory($path);
1273
                $path .= DIRECTORY_SEPARATOR;
1274
            } elseif ($depth === 0) {
1275
                $path = '';
1276
            } elseif ($depth !== 0) {
1277
                $path .= DIRECTORY_SEPARATOR.$parent->getName().DIRECTORY_SEPARATOR;
1278
            }
1279
1280
            foreach ($children as $child) {
1281
                $name = $path.$child->getName();
1282
1283
                if ($child instanceof Collection) {
1284
                    $archive->addDirectory($name);
1285
                    $this->zip($archive, $self, $child, $name, ++$depth);
1286
                } elseif ($child instanceof File) {
1287
                    try {
1288
                        $archive->addFile($child->get(), $name);
0 ignored issues
show
Documentation introduced by
$child->get() is of type resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1289
                    } catch (\Exception $e) {
1290
                        $this->_logger->error('failed add file ['.$child->getId().'] to zip stream', array(
1291
                            'category' => get_class($this),
1292
                            'exception' => $e,
1293
                        ));
1294
                    }
1295
                }
1296
            }
1297
        } elseif ($parent instanceof File) {
1298
            $archive->addFile($parent->get(), $parent->getName());
0 ignored issues
show
Documentation introduced by
$parent->get() is of type resource, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1299
        }
1300
1301
        return true;
1302
    }
1303
1304
1305
    /**
1306
     * Is reference
1307
     *
1308
     *  @return bool
1309
     */
1310
    public function isReference(): bool
1311
    {
1312
        return ($this->reference instanceof ObjectId);
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\ObjectId does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1313
    }
1314
1315
1316
    /**
1317
     * Set meta attribute
1318
     *
1319
     * @param   array|string
1320
     * @param   mixed $value
1321
     * @return  INode
1322
     */
1323
    public function setMetaAttribute($attributes, $value=null): INode
1324
    {
1325
        $this->meta = self::validateMetaAttribute($attributes, $value, $this->meta);
1326
        $this->save('meta');
1327
        return $this;
1328
    }
1329
1330
1331
    /**
1332
     * validate meta attribut
1333
     *
1334
     * @param   array|string $attributes
1335
     * @param   mixed $value
1336
     * @param   array $set
1337
     * @return  array
1338
     */
1339
    public static function validateMetaAttribute($attributes, $value=null, array $set=[]): array
1340
    {
1341
        if (is_string($attributes)) {
1342
            $attributes = [
1343
                $attributes => $value,
1344
            ];
1345
        }
1346
1347
        foreach ($attributes as $attribute => $value) {
1348
            $const = __CLASS__.'::META_'.strtoupper($attribute);
1349
            if (!defined($const)) {
1350
                throw new Exception('meta attribute '.$attribute.' is not valid');
1351
            }
1352
1353
            if (empty($value) && array_key_exists($attribute, $set)) {
1354
                unset($set[$attribute]);
1355
            } else {
1356
                $set[$attribute] = $value;
1357
            }
1358
        }
1359
1360
        return $set;
1361
    }
1362
1363
1364
    /**
1365
     * Get meta attributes as array
1366
     *
1367
     * @param  string|array $attribute Specify attributes to return
1368
     * @return string|array
1369
     */
1370
    public function getMetaAttribute($attribute=[])
1371
    {
1372
        if (is_string($attribute)) {
1373
            if (isset($this->meta[$attribute])) {
1374
                return $this->meta[$attribute];
1375
            }
1376
        } elseif (empty($attribute)) {
1377
            return $this->meta;
1378
        } elseif (is_array($attribute)) {
1379
            return array_intersect_key($this->meta, array_flip($attribute));
1380
        }
1381
    }
1382
1383
    
1384
    /**
1385
     * Mark node as readonly
1386
     *
1387
     * @param   bool $readonly
1388
     * @return  bool
1389
     */
1390
    public function setReadonly(bool $readonly=true): bool
1391
    {
1392
        $this->readonly = $readonly;
1393
        return $this->save('readonly');
1394
    }
1395
1396
    
1397
    /**
1398
     * Mark node as self-destroyable
1399
     *
1400
     * @param   UTCDateTime $ts
1401
     * @return  bool
1402
     */
1403
    public function setDestroyable(?UTCDateTime $ts): bool
1404
    {
1405
        $this->destroy = $ts;
1406
        
1407
        if ($ts === null) {
1408
            return $this->save([], 'destroy');
1409
        } else {
1410
            return $this->save('destroy');
1411
        }
1412
    }
1413
1414
1415
    /**
1416
     * Delete node
1417
     *
1418
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
1419
     * delete finally
1420
     *
1421
     * @param   bool $force
1422
     * @param   bool $recursion_first
1423
     * @param   string $recursion
1424
     * @return  bool
1425
     */
1426
    abstract public function delete(bool $force=false, ?string $recursion=null, bool $recursion_first=true): bool;
1427
1428
1429
    /**
1430
     * Get original raw attributes before any processing
1431
     *
1432
     * @return array|\MongoDB\BSON\Document
1433
     */
1434
    public function getRawAttributes()
1435
    {
1436
        return $this->raw_attributes;
1437
    }
1438
1439
1440
    /**
1441
     * Completly remove node
1442
     *
1443
     * @return bool
1444
     */
1445
    abstract protected function _forceDelete(): bool;
1446
1447
1448
    /**
1449
     * Check if node is in root
1450
     *
1451
     * @return bool
1452
     */
1453
    public function isInRoot(): bool
1454
    {
1455
        return $this->parent === null;
1456
    }
1457
1458
    
1459
    /**
1460
     * Check if node is an instance of the actual root collection
1461
     *
1462
     * @return bool
1463
     */
1464
    public function isRoot(): bool
1465
    {
1466
        return $this->_id === null && ($this instanceof Collection);
1467
    }
1468
1469
1470
    /**
1471
     * Resolve node path
1472
     *
1473
     * @return string
1474
     */
1475
    public function getPath(): string
1476
    {
1477
        $path = '';
1478
        foreach (array_reverse($this->getParents()) as $parent) {
1479
            $path .= DIRECTORY_SEPARATOR.$parent->getName();
1480
        }
1481
1482
        $path .= DIRECTORY_SEPARATOR.$this->getName();
1483
        return $path;
1484
    }
1485
1486
1487
    /**
1488
     * Save node attributes
1489
     *
1490
     * @param  string|array $attributes
1491
     * @param  string|array $remove
1492
     * @param  string $recursion
1493
     * @param  bool $recursion_first
1494
     * @return bool
1495
     */
1496
    public function save($attributes=[], $remove=[], ?string $recursion=null, bool $recursion_first=true): bool
1497
    {
1498
        if (!$this->isAllowed('w') && !$this->isReference()) {
1499
            throw new Exception\Forbidden('not allowed to modify node '.$this->name,
1500
                Exception\Forbidden::NOT_ALLOWED_TO_MODIFY
1501
            );
1502
        }
1503
        
1504
        $remove     = (array)$remove;
1505
        $attributes = (array)$attributes;
1506
        $this->_pluginmgr->run('preSaveNodeAttributes',
1507
            [$this, &$attributes, &$remove, &$recursion, &$recursion_first]);
1508
1509
        try {
1510
            $set = [];
1511
1512
            foreach ($attributes as $attr) {
1513
                $set[$attr] = $this->{$attr};
1514
            }
1515
1516
            $update = [];
1517
            if (!empty($set)) {
1518
                $update['$set'] = $set;
1519
            }
1520
1521
            if (!empty($remove)) {
1522
                $remove = array_fill_keys($remove, 1);
1523
                $update['$unset'] = $remove;
1524
            }
1525
1526
            if (empty($update)) {
1527
                return false;
1528
            } else {
1529
                $result =$this->_db->storage->updateOne([
0 ignored issues
show
Unused Code introduced by
$result 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...
1530
                    '_id' => $this->_id,
1531
                ], $update);
1532
            }
1533
            
1534
            $this->_pluginmgr->run('postSaveNodeAttributes',
1535
                [$this, $attributes, $remove, $recursion, $recursion_first]);
1536
            
1537
            $this->_logger->info('modified node attributes of ['.$this->_id.']', [
1538
                'category' => get_class($this),
1539
                'params'   => $update,
1540
            ]);
1541
       
1542
            return true;
1543
        } catch (\Exception $e) {
1544
            $this->_logger->error('failed modify node attributes of ['.$this->_id.']', [
1545
                'category' => get_class($this),
1546
                'exception' => $e
1547
            ]);
1548
1549
            throw $e;
1550
        }
1551
    }
1552
1553
1554
    /**
1555
     * Get children
1556
     *
1557
     * @param   array $filter
1558
     * @param   array $attributes
1559
     * @param   int $limit
1560
     * @param   string $cursor
1561
     * @param   bool $has_more
1562
     * @return  array
1563
     */
1564
    public static function loadNodeAttributesWithCustomFilter(
1565
        ?array $filter = null,
1566
        array $attributes = ['_id'],
1567
        ?int $limit = null,
1568
        ?int &$cursor = null,
1569
        ?bool &$has_more = null)
1570
    {
1571
        $default = [
1572
            '_id'       => 1,
1573
            'directory' => 1,
1574
            'shared'    => 1,
1575
            'name'      => 1,
1576
            'parent'    => 1,
1577
        ];
1578
1579
        $search_attributes = array_merge($default, array_fill_keys($attributes, 1));
1580
        $list   = [];
1581
        $result =$this->_db->storage->find($filter, [
0 ignored issues
show
Bug introduced by
The variable $this does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1582
            'skip'      => $cursor,
1583
            'limit'     => $limit,
1584
            'projection'=> $search_attributes
1585
        ]);
1586
1587
        $cursor += $limit;
1588
1589
        $result = $result->toArray();
1590
        $count  = count($result);
1591
        
1592
        if ($cursor > $count) {
1593
            $cursor = $count;
1594
        }
1595
        
1596
        $has_more = ($cursor < $count);
1597
        
1598
        foreach ($result as $node) {
1599
            if ($node['directory'] === true) {
1600
                $node = new Collection($node);
0 ignored issues
show
Bug introduced by
The call to Collection::__construct() misses a required argument $fs.

This check looks for function calls that miss required arguments.

Loading history...
1601
            } else {
1602
                $node = new File($node);
0 ignored issues
show
Bug introduced by
The call to File::__construct() misses a required argument $fs.

This check looks for function calls that miss required arguments.

Loading history...
1603
            }
1604
1605
            $values = $node->getAttribute($attributes);
1606
            $list[] = $values;
1607
        }
1608
1609
        return $list;
1610
    }
1611
}
1612