Completed
Push — master ( afaf42...8ac772 )
by Raffael
20:10 queued 16:21
created

ExternalStorage::addTasks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 0
cts 15
cp 0
rs 9.7
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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\Hook;
13
14
use Balloon\Async\SmbListener;
15
use Balloon\Async\SmbScanner;
16
use Balloon\Filesystem\Exception\NotFound as NotFoundException;
17
use Balloon\Filesystem\Node\Collection;
18
use Balloon\Filesystem\Storage\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Hook\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
19
use Balloon\Filesystem\Storage\Factory as StorageFactory;
20
use Balloon\Server;
21
use InvalidArgumentException;
22
use MongoDB\BSON\ObjectId;
23
use ParagonIE\Halite\HiddenString;
24
use ParagonIE\Halite\KeyFactory;
25
use ParagonIE\Halite\Symmetric\Crypto as Symmetric;
26
use ParagonIE\Halite\Symmetric\EncryptionKey;
27
use Psr\Log\LoggerInterface;
28
use TaskScheduler\JobInterface;
29
use TaskScheduler\Scheduler;
30
31
class ExternalStorage extends AbstractHook
32
{
33
    /**
34
     * Default key.
35
     */
36
    private const DEFAULT_KEY = '3140040033da9bd0dedd8babc8b89cda7f2132dd5009cc43c619382863d0c75e172ebf18e713e1987f35d6ea3ace43b561c50d9aefc4441a8c4418f6928a70e4655de5a9660cd323de63b4fd2fb76525470f25311c788c5e366e29bf60c438c4ac0b440e';
37
38
    /**
39
     * Server.
40
     *
41
     * @var Server
42
     */
43
    protected $server;
44
45
    /**
46
     * Scheduler.
47
     *
48
     * @var Scheduler
49
     */
50
    protected $scheduler;
51
52
    /**
53
     * Storage factory.
54
     *
55
     * @var StorageFactory
56
     */
57
    protected $factory;
58
59
    /**
60
     * Encryption key.
61
     *
62
     * @var EncryptionKey
63
     */
64
    protected $key;
65
66
    /**
67
     * Logger.
68
     *
69
     * @var LoggerInterface
70
     */
71
    protected $logger;
72
73
    /**
74
     * Interval.
75
     */
76
    protected $interval = 86400;
77
78
    /**
79
     * Constructor.
80
     */
81
    public function __construct(Server $server, Scheduler $scheduler, StorageFactory $factory, EncryptionKey $key, LoggerInterface $logger, ?array $config = null)
82
    {
83
        $this->server = $server;
84
        $this->scheduler = $scheduler;
85
        $this->factory = $factory;
86
        $this->key = $key;
87
        $this->logger = $logger;
88
        $this->setOptions($config);
89
    }
90
91
    /**
92
     * Set options.
93
     */
94
    public function setOptions(?Iterable $config = null): HookInterface
95
    {
96
        if (null === $config) {
97
            return $this;
98
        }
99
100
        foreach ($config as $option => $value) {
101
            switch ($option) {
102
                case 'interval':
103
                    $this->{$option} = (int) $value;
104
105
                break;
106
                default:
107
                    throw new InvalidArgumentException('invalid option '.$option.' given');
108
            }
109
        }
110
111
        return $this;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    public function preCreateCollection(Collection $parent, string &$name, array &$attributes, bool $clone): void
118
    {
119
        if (!isset($attributes['mount'])) {
120
            return;
121
        }
122
123
        if (isset($attributes['mount']['password'])) {
124
            if (KeyFactory::export($this->key)->getString() === self::DEFAULT_KEY) {
125
                throw new Exception\InvalidEncryptionKey('smb encryption key required to be changed');
126
            }
127
128
            $message = new HiddenString($attributes['mount']['password']);
129
            $attributes['mount']['password'] = Symmetric::encrypt($message, $this->key);
130
        }
131
132
        $this->factory->build($attributes['mount'])->test();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Balloon\Filesystem\Stora...dapter\AdapterInterface as the method test() does only exist in the following implementations of said interface: Balloon\Filesystem\Storage\Adapter\Smb.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138
    public function postCreateCollection(Collection $parent, Collection $node, bool $clone): void
139
    {
140
        if (!$node->isMounted()) {
141
            return;
142
        }
143
144
        $this->addTasks($node->getId());
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     */
150
    public function preDeleteCollection(Collection $node, bool $force, ?string $recursion, bool $recursion_first): void
151
    {
152
        if ($node->isMounted() && $force === true) {
153
            throw new \Exception('mounted collection can not get removed');
154
        }
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160
    public function postDeleteCollection(Collection $node, bool $force, ?string $recursion, bool $recursion_first): void
161
    {
162
        if (!$node->isMounted()) {
163
            return;
164
        }
165
166
        foreach ($this->scheduler->getJobs([
167
            '$or' => [['class' => SmbScanner::class], ['class' => SmbListener::class]],
168
            'data.id' => $node->getId(),
169
            'status' => ['$lte' => JobInterface::STATUS_PROCESSING],
170
        ]) as $job) {
171
            $this->scheduler->cancelJob($job->getId());
172
        }
173
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178
    public function preExecuteAsyncJobs(): void
179
    {
180
        $fs = $this->server->getFilesystem();
181
182
        foreach ($this->scheduler->getJobs([
183
            '$or' => [['class' => SmbScanner::class], ['class' => SmbListener::class]],
184
            'status' => ['$lte' => JobInterface::STATUS_PROCESSING],
185
        ]) as $job) {
186
            try {
187
                if ($fs->findNodeById($job->getData()['id'])->isDeleted()) {
188
                    $this->scheduler->cancelJob($job->getId());
189
                }
190
            } catch (NotFoundException $e) {
191
                $this->scheduler->cancelJob($job->getId());
192
            } catch (\Exception $e) {
193
                $this->logger->error('failed pre check mount job ['.$job->getId().']', [
194
                    'category' => get_class($this),
195
                    'exception' => $e,
196
                ]);
197
            }
198
        }
199
200
        $nodes = $fs->findNodesByFilter([
201
            'deleted' => false,
202
            'directory' => true,
203
            'mount' => ['$type' => 3],
204
        ]);
205
206
        foreach ($nodes as $node) {
207
            $this->addTasks($node->getId());
208
        }
209
    }
210
211
    /**
212
     * Add tasks.
213
     */
214
    protected function addTasks(ObjectId $node): bool
215
    {
216
        $this->scheduler->addJobOnce(SmbScanner::class, [
217
            'id' => $node,
218
        ], [
219
            Scheduler::OPTION_INTERVAL => $this->interval,
220
        ]);
221
222
        $job = $this->scheduler->addJobOnce(SmbListener::class, [
0 ignored issues
show
Unused Code introduced by
$job 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...
223
            'id' => $node,
224
        ], [
225
            Scheduler::OPTION_IGNORE_MAX_CHILDREN => true,
226
            Scheduler::OPTION_RETRY => -1,
227
        ]);
228
229
        return true;
230
    }
231
}
232