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 const DIRECTORY_SEPARATOR; |
||||
14 | use const LOCK_EX; |
||||
15 | use function array_filter; |
||||
16 | use function array_map; |
||||
17 | use function chmod; |
||||
18 | use function file_get_contents; |
||||
19 | use function file_put_contents; |
||||
20 | use function fileatime; |
||||
21 | use function glob; |
||||
22 | use function is_dir; |
||||
23 | use function is_file; |
||||
24 | use function is_writable; |
||||
25 | use function mkdir; |
||||
26 | use function sprintf; |
||||
27 | use function time; |
||||
28 | use function unlink; |
||||
29 | |||||
30 | /** |
||||
31 | * Very naive concurrent region, based on file locks. |
||||
32 | */ |
||||
33 | class FileLockRegion implements ConcurrentRegion |
||||
34 | { |
||||
35 | public const LOCK_EXTENSION = 'lock'; |
||||
36 | |||||
37 | /** |
||||
38 | * @var Region |
||||
39 | */ |
||||
40 | private $region; |
||||
41 | |||||
42 | /** |
||||
43 | * @var string |
||||
44 | */ |
||||
45 | private $directory; |
||||
46 | |||||
47 | /** |
||||
48 | * @var int |
||||
49 | */ |
||||
50 | private $lockLifetime; |
||||
51 | |||||
52 | /** |
||||
53 | * @param string $directory |
||||
54 | * @param string $lockLifetime |
||||
55 | * |
||||
56 | * @throws \InvalidArgumentException |
||||
57 | */ |
||||
58 | 12 | public function __construct(Region $region, $directory, $lockLifetime) |
|||
59 | { |
||||
60 | 12 | if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) { |
|||
61 | throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory)); |
||||
62 | } |
||||
63 | |||||
64 | 12 | if (! is_writable($directory)) { |
|||
65 | throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory)); |
||||
66 | } |
||||
67 | |||||
68 | 12 | $this->region = $region; |
|||
69 | 12 | $this->directory = $directory; |
|||
70 | 12 | $this->lockLifetime = $lockLifetime; |
|||
0 ignored issues
–
show
|
|||||
71 | 12 | } |
|||
72 | |||||
73 | /** |
||||
74 | * @return bool |
||||
75 | */ |
||||
76 | 10 | private function isLocked(CacheKey $key, ?Lock $lock = null) |
|||
77 | { |
||||
78 | 10 | $filename = $this->getLockFileName($key); |
|||
79 | |||||
80 | 10 | if (! is_file($filename)) { |
|||
81 | 10 | return false; |
|||
82 | } |
||||
83 | |||||
84 | 6 | $time = $this->getLockTime($filename); |
|||
85 | 6 | $content = $this->getLockContent($filename); |
|||
86 | |||||
87 | 6 | if (! $content || ! $time) { |
|||
88 | @unlink($filename); |
||||
0 ignored issues
–
show
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
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.');
}
![]() |
|||||
89 | |||||
90 | return false; |
||||
91 | } |
||||
92 | |||||
93 | 6 | if ($lock && $content === $lock->value) { |
|||
94 | 1 | return false; |
|||
95 | } |
||||
96 | |||||
97 | // outdated lock |
||||
98 | 6 | if (($time + $this->lockLifetime) <= time()) { |
|||
99 | 1 | @unlink($filename); |
|||
100 | |||||
101 | 1 | return false; |
|||
102 | } |
||||
103 | |||||
104 | 5 | return true; |
|||
105 | } |
||||
106 | |||||
107 | /** |
||||
108 | * @return string |
||||
109 | */ |
||||
110 | 10 | private function getLockFileName(CacheKey $key) |
|||
111 | { |
||||
112 | 10 | return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; |
|||
113 | } |
||||
114 | |||||
115 | /** |
||||
116 | * @param string $filename |
||||
117 | * |
||||
118 | * @return string |
||||
119 | */ |
||||
120 | 6 | private function getLockContent($filename) |
|||
121 | { |
||||
122 | 6 | return @file_get_contents($filename); |
|||
123 | } |
||||
124 | |||||
125 | /** |
||||
126 | * @param string $filename |
||||
127 | * |
||||
128 | * @return int |
||||
129 | */ |
||||
130 | 6 | private function getLockTime($filename) |
|||
131 | { |
||||
132 | 6 | return @fileatime($filename); |
|||
133 | } |
||||
134 | |||||
135 | /** |
||||
136 | * {@inheritdoc} |
||||
137 | */ |
||||
138 | 1 | public function getName() |
|||
139 | { |
||||
140 | 1 | return $this->region->getName(); |
|||
141 | } |
||||
142 | |||||
143 | /** |
||||
144 | * {@inheritdoc} |
||||
145 | */ |
||||
146 | 10 | public function contains(CacheKey $key) |
|||
147 | { |
||||
148 | 10 | if ($this->isLocked($key)) { |
|||
149 | 5 | return false; |
|||
150 | } |
||||
151 | |||||
152 | 10 | return $this->region->contains($key); |
|||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * {@inheritdoc} |
||||
157 | */ |
||||
158 | 6 | public function get(CacheKey $key) |
|||
159 | { |
||||
160 | 6 | if ($this->isLocked($key)) { |
|||
161 | 3 | return null; |
|||
162 | } |
||||
163 | |||||
164 | 3 | return $this->region->get($key); |
|||
165 | } |
||||
166 | |||||
167 | /** |
||||
168 | * {@inheritdoc} |
||||
169 | */ |
||||
170 | public function getMultiple(CollectionCacheEntry $collection) |
||||
171 | { |
||||
172 | if (array_filter(array_map([$this, 'isLocked'], $collection->identifiers))) { |
||||
173 | return null; |
||||
174 | } |
||||
175 | |||||
176 | return $this->region->getMultiple($collection); |
||||
177 | } |
||||
178 | |||||
179 | /** |
||||
180 | * {@inheritdoc} |
||||
181 | */ |
||||
182 | 10 | public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null) |
|||
183 | { |
||||
184 | 10 | if ($this->isLocked($key, $lock)) { |
|||
185 | 1 | return false; |
|||
186 | } |
||||
187 | |||||
188 | 10 | return $this->region->put($key, $entry); |
|||
189 | } |
||||
190 | |||||
191 | /** |
||||
192 | * {@inheritdoc} |
||||
193 | */ |
||||
194 | 3 | public function evict(CacheKey $key) |
|||
195 | { |
||||
196 | 3 | if ($this->isLocked($key)) { |
|||
197 | 1 | @unlink($this->getLockFileName($key)); |
|||
0 ignored issues
–
show
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
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.');
}
![]() |
|||||
198 | } |
||||
199 | |||||
200 | 3 | return $this->region->evict($key); |
|||
201 | } |
||||
202 | |||||
203 | /** |
||||
204 | * {@inheritdoc} |
||||
205 | */ |
||||
206 | 3 | public function evictAll() |
|||
207 | { |
||||
208 | // The check below is necessary because on some platforms glob returns false |
||||
209 | // when nothing matched (even though no errors occurred) |
||||
210 | 3 | $filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)); |
|||
211 | |||||
212 | 3 | if ($filenames) { |
|||
213 | 1 | foreach ($filenames as $filename) { |
|||
214 | 1 | @unlink($filename); |
|||
215 | } |
||||
216 | } |
||||
217 | |||||
218 | 3 | return $this->region->evictAll(); |
|||
219 | } |
||||
220 | |||||
221 | /** |
||||
222 | * {@inheritdoc} |
||||
223 | */ |
||||
224 | 6 | public function lock(CacheKey $key) |
|||
225 | { |
||||
226 | 6 | if ($this->isLocked($key)) { |
|||
227 | 1 | return null; |
|||
228 | } |
||||
229 | |||||
230 | 5 | $lock = Lock::createLockRead(); |
|||
231 | 5 | $filename = $this->getLockFileName($key); |
|||
232 | |||||
233 | 5 | if (! @file_put_contents($filename, $lock->value, LOCK_EX)) { |
|||
234 | return null; |
||||
235 | } |
||||
236 | 5 | chmod($filename, 0664); |
|||
237 | |||||
238 | 5 | return $lock; |
|||
239 | } |
||||
240 | |||||
241 | /** |
||||
242 | * {@inheritdoc} |
||||
243 | */ |
||||
244 | 2 | public function unlock(CacheKey $key, Lock $lock) |
|||
245 | { |
||||
246 | 2 | if ($this->isLocked($key, $lock)) { |
|||
247 | 1 | return false; |
|||
0 ignored issues
–
show
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.
}
}
![]() |
|||||
248 | } |
||||
249 | |||||
250 | 1 | if (! @unlink($this->getLockFileName($key))) { |
|||
251 | return false; |
||||
0 ignored issues
–
show
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.
}
}
![]() |
|||||
252 | } |
||||
253 | |||||
254 | 1 | return true; |
|||
0 ignored issues
–
show
The expression
return true returns the type true 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.
}
}
![]() |
|||||
255 | } |
||||
256 | } |
||||
257 |
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.