Passed
Pull Request — master (#7648)
by Ahmed M.
09:27
created

FileLockRegion::unlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Cache\Region;
6
7
use Doctrine\ORM\Cache\CacheEntry;
8
use Doctrine\ORM\Cache\CacheKey;
9
use Doctrine\ORM\Cache\CollectionCacheEntry;
10
use Doctrine\ORM\Cache\ConcurrentRegion;
11
use Doctrine\ORM\Cache\Lock;
12
use Doctrine\ORM\Cache\Region;
13
use InvalidArgumentException;
14
use const DIRECTORY_SEPARATOR;
15
use const LOCK_EX;
16
use function array_filter;
17
use function array_map;
18
use function chmod;
19
use function file_get_contents;
20
use function file_put_contents;
21
use function fileatime;
22
use function glob;
23
use function is_dir;
24
use function is_file;
25
use function is_writable;
26
use function mkdir;
27
use function sprintf;
28
use function time;
29
use function unlink;
30
31
/**
32
 * Very naive concurrent region, based on file locks.
33
 */
34
class FileLockRegion implements ConcurrentRegion
35
{
36
    public const LOCK_EXTENSION = 'lock';
37
38
    /** @var Region */
39
    private $region;
40
41
    /** @var string */
42
    private $directory;
43
44
    /** @var int */
45
    private $lockLifetime;
46
47
    /**
48
     * @param string $directory
49
     * @param string $lockLifetime
50
     *
51
     * @throws InvalidArgumentException
52
     */
53 12
    public function __construct(Region $region, $directory, $lockLifetime)
54
    {
55 12
        if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) {
56
            throw new InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory));
57
        }
58
59 12
        if (! is_writable($directory)) {
60
            throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory));
61
        }
62
63 12
        $this->region       = $region;
64 12
        $this->directory    = $directory;
65 12
        $this->lockLifetime = $lockLifetime;
0 ignored issues
show
Documentation Bug introduced by
The property $lockLifetime was declared of type integer, but $lockLifetime is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
66 12
    }
67
68
    /**
69
     * @return bool
70
     */
71 10
    private function isLocked(CacheKey $key, ?Lock $lock = null)
72
    {
73 10
        $filename = $this->getLockFileName($key);
74
75 10
        if (! is_file($filename)) {
76 10
            return false;
77
        }
78
79 6
        $time    = $this->getLockTime($filename);
80 6
        $content = $this->getLockContent($filename);
81
82 6
        if (! $content || ! $time) {
83
            @unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

83
            /** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
84
85
            return false;
86
        }
87
88 6
        if ($lock && $content === $lock->value) {
89 1
            return false;
90
        }
91
92
        // outdated lock
93 6
        if (($time + $this->lockLifetime) <= time()) {
94 1
            @unlink($filename);
95
96 1
            return false;
97
        }
98
99 5
        return true;
100
    }
101
102
    /**
103
     * @return string
104
     */
105 10
    private function getLockFileName(CacheKey $key)
106
    {
107 10
        return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION;
108
    }
109
110
    /**
111
     * @param string $filename
112
     *
113
     * @return string
114
     */
115 6
    private function getLockContent($filename)
116
    {
117 6
        return @file_get_contents($filename);
118
    }
119
120
    /**
121
     * @param string $filename
122
     *
123
     * @return int
124
     */
125 6
    private function getLockTime($filename)
126
    {
127 6
        return @fileatime($filename);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 1
    public function getName()
134
    {
135 1
        return $this->region->getName();
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141 10
    public function contains(CacheKey $key)
142
    {
143 10
        if ($this->isLocked($key)) {
144 5
            return false;
145
        }
146
147 10
        return $this->region->contains($key);
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 6
    public function get(CacheKey $key)
154
    {
155 6
        if ($this->isLocked($key)) {
156 3
            return null;
157
        }
158
159 3
        return $this->region->get($key);
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165
    public function getMultiple(CollectionCacheEntry $collection)
166
    {
167
        if (array_filter(array_map([$this, 'isLocked'], $collection->identifiers))) {
168
            return null;
169
        }
170
171
        return $this->region->getMultiple($collection);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 10
    public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null)
178
    {
179 10
        if ($this->isLocked($key, $lock)) {
180 1
            return false;
181
        }
182
183 10
        return $this->region->put($key, $entry);
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189 3
    public function evict(CacheKey $key)
190
    {
191 3
        if ($this->isLocked($key)) {
192 1
            @unlink($this->getLockFileName($key));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

192
            /** @scrutinizer ignore-unhandled */ @unlink($this->getLockFileName($key));

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
193
        }
194
195 3
        return $this->region->evict($key);
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 3
    public function evictAll()
202
    {
203
        // The check below is necessary because on some platforms glob returns false
204
        // when nothing matched (even though no errors occurred)
205 3
        $filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION));
206
207 3
        if ($filenames) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filenames of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
208 1
            foreach ($filenames as $filename) {
209 1
                @unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

209
                /** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
210
            }
211
        }
212
213 3
        return $this->region->evictAll();
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 6
    public function lock(CacheKey $key)
220
    {
221 6
        if ($this->isLocked($key)) {
222 1
            return null;
223
        }
224
225 5
        $lock     = Lock::createLockRead();
226 5
        $filename = $this->getLockFileName($key);
227
228 5
        if (! @file_put_contents($filename, $lock->value, LOCK_EX)) {
229
            return null;
230
        }
231 5
        chmod($filename, 0664);
232
233 5
        return $lock;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 2
    public function unlock(CacheKey $key, Lock $lock)
240
    {
241 2
        if ($this->isLocked($key, $lock)) {
242 1
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by Doctrine\ORM\Cache\ConcurrentRegion::unlock() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
243
        }
244
245 1
        return @unlink($this->getLockFileName($key));
0 ignored issues
show
Bug Best Practice introduced by
The expression return @unlink($this->getLockFileName($key)) returns the type boolean which is incompatible with the return type mandated by Doctrine\ORM\Cache\ConcurrentRegion::unlock() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
246
    }
247
}
248