This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Proxy backend that mirrors writes to several internal backends. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup FileBackend |
||
22 | * @author Aaron Schulz |
||
23 | */ |
||
24 | |||
25 | /** |
||
26 | * @brief Proxy backend that mirrors writes to several internal backends. |
||
27 | * |
||
28 | * This class defines a multi-write backend. Multiple backends can be |
||
29 | * registered to this proxy backend and it will act as a single backend. |
||
30 | * Use this when all access to those backends is through this proxy backend. |
||
31 | * At least one of the backends must be declared the "master" backend. |
||
32 | * |
||
33 | * Only use this class when transitioning from one storage system to another. |
||
34 | * |
||
35 | * Read operations are only done on the 'master' backend for consistency. |
||
36 | * Write operations are performed on all backends, starting with the master. |
||
37 | * This makes a best-effort to have transactional semantics, but since requests |
||
38 | * may sometimes fail, the use of "autoResync" or background scripts to fix |
||
39 | * inconsistencies is important. |
||
40 | * |
||
41 | * @ingroup FileBackend |
||
42 | * @since 1.19 |
||
43 | */ |
||
44 | class FileBackendMultiWrite extends FileBackend { |
||
45 | /** @var FileBackendStore[] Prioritized list of FileBackendStore objects */ |
||
46 | protected $backends = []; |
||
47 | |||
48 | /** @var int Index of master backend */ |
||
49 | protected $masterIndex = -1; |
||
50 | /** @var int Index of read affinity backend */ |
||
51 | protected $readIndex = -1; |
||
52 | |||
53 | /** @var int Bitfield */ |
||
54 | protected $syncChecks = 0; |
||
55 | /** @var string|bool */ |
||
56 | protected $autoResync = false; |
||
57 | |||
58 | /** @var bool */ |
||
59 | protected $asyncWrites = false; |
||
60 | |||
61 | /* Possible internal backend consistency checks */ |
||
62 | const CHECK_SIZE = 1; |
||
63 | const CHECK_TIME = 2; |
||
64 | const CHECK_SHA1 = 4; |
||
65 | |||
66 | /** |
||
67 | * Construct a proxy backend that consists of several internal backends. |
||
68 | * Locking, journaling, and read-only checks are handled by the proxy backend. |
||
69 | * |
||
70 | * Additional $config params include: |
||
71 | * - backends : Array of backend config and multi-backend settings. |
||
72 | * Each value is the config used in the constructor of a |
||
73 | * FileBackendStore class, but with these additional settings: |
||
74 | * - class : The name of the backend class |
||
75 | * - isMultiMaster : This must be set for one backend. |
||
76 | * - readAffinity : Use this for reads without 'latest' set. |
||
77 | * - syncChecks : Integer bitfield of internal backend sync checks to perform. |
||
78 | * Possible bits include the FileBackendMultiWrite::CHECK_* constants. |
||
79 | * There are constants for SIZE, TIME, and SHA1. |
||
80 | * The checks are done before allowing any file operations. |
||
81 | * - autoResync : Automatically resync the clone backends to the master backend |
||
82 | * when pre-operation sync checks fail. This should only be used |
||
83 | * if the master backend is stable and not missing any files. |
||
84 | * Use "conservative" to limit resyncing to copying newer master |
||
85 | * backend files over older (or non-existing) clone backend files. |
||
86 | * Cases that cannot be handled will result in operation abortion. |
||
87 | * - replication : Set to 'async' to defer file operations on the non-master backends. |
||
88 | * This will apply such updates post-send for web requests. Note that |
||
89 | * any checks from "syncChecks" are still synchronous. |
||
90 | * |
||
91 | * @param array $config |
||
92 | * @throws FileBackendError |
||
93 | */ |
||
94 | public function __construct( array $config ) { |
||
95 | parent::__construct( $config ); |
||
96 | $this->syncChecks = isset( $config['syncChecks'] ) |
||
97 | ? $config['syncChecks'] |
||
98 | : self::CHECK_SIZE; |
||
99 | $this->autoResync = isset( $config['autoResync'] ) |
||
100 | ? $config['autoResync'] |
||
101 | : false; |
||
102 | $this->asyncWrites = isset( $config['replication'] ) && $config['replication'] === 'async'; |
||
103 | // Construct backends here rather than via registration |
||
104 | // to keep these backends hidden from outside the proxy. |
||
105 | $namesUsed = []; |
||
106 | foreach ( $config['backends'] as $index => $config ) { |
||
107 | $name = $config['name']; |
||
108 | if ( isset( $namesUsed[$name] ) ) { // don't break FileOp predicates |
||
109 | throw new LogicException( "Two or more backends defined with the name $name." ); |
||
110 | } |
||
111 | $namesUsed[$name] = 1; |
||
112 | // Alter certain sub-backend settings for sanity |
||
113 | unset( $config['readOnly'] ); // use proxy backend setting |
||
114 | unset( $config['fileJournal'] ); // use proxy backend journal |
||
115 | unset( $config['lockManager'] ); // lock under proxy backend |
||
116 | $config['domainId'] = $this->domainId; // use the proxy backend wiki ID |
||
117 | if ( !empty( $config['isMultiMaster'] ) ) { |
||
118 | if ( $this->masterIndex >= 0 ) { |
||
119 | throw new LogicException( 'More than one master backend defined.' ); |
||
120 | } |
||
121 | $this->masterIndex = $index; // this is the "master" |
||
0 ignored issues
–
show
|
|||
122 | $config['fileJournal'] = $this->fileJournal; // log under proxy backend |
||
123 | } |
||
124 | if ( !empty( $config['readAffinity'] ) ) { |
||
125 | $this->readIndex = $index; // prefer this for reads |
||
0 ignored issues
–
show
It seems like
$index can also be of type string . However, the property $readIndex is declared as type integer . 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 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;
}
![]() |
|||
126 | } |
||
127 | // Create sub-backend object |
||
128 | if ( !isset( $config['class'] ) ) { |
||
129 | throw new InvalidArgumentException( 'No class given for a backend config.' ); |
||
130 | } |
||
131 | $class = $config['class']; |
||
132 | $this->backends[$index] = new $class( $config ); |
||
133 | } |
||
134 | if ( $this->masterIndex < 0 ) { // need backends and must have a master |
||
135 | throw new LogicException( 'No master backend defined.' ); |
||
136 | } |
||
137 | if ( $this->readIndex < 0 ) { |
||
138 | $this->readIndex = $this->masterIndex; // default |
||
0 ignored issues
–
show
It seems like
$this->masterIndex can also be of type string . However, the property $readIndex is declared as type integer . 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 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;
}
![]() |
|||
139 | } |
||
140 | } |
||
141 | |||
142 | final protected function doOperationsInternal( array $ops, array $opts ) { |
||
143 | $status = $this->newStatus(); |
||
144 | |||
145 | $mbe = $this->backends[$this->masterIndex]; // convenience |
||
146 | |||
147 | // Try to lock those files for the scope of this function... |
||
148 | $scopeLock = null; |
||
149 | if ( empty( $opts['nonLocking'] ) ) { |
||
150 | // Try to lock those files for the scope of this function... |
||
151 | /** @noinspection PhpUnusedLocalVariableInspection */ |
||
152 | $scopeLock = $this->getScopedLocksForOps( $ops, $status ); |
||
153 | if ( !$status->isOK() ) { |
||
154 | return $status; // abort |
||
155 | } |
||
156 | } |
||
157 | // Clear any cache entries (after locks acquired) |
||
158 | $this->clearCache(); |
||
159 | $opts['preserveCache'] = true; // only locked files are cached |
||
160 | // Get the list of paths to read/write... |
||
161 | $relevantPaths = $this->fileStoragePathsForOps( $ops ); |
||
162 | // Check if the paths are valid and accessible on all backends... |
||
163 | $status->merge( $this->accessibilityCheck( $relevantPaths ) ); |
||
164 | if ( !$status->isOK() ) { |
||
165 | return $status; // abort |
||
166 | } |
||
167 | // Do a consistency check to see if the backends are consistent... |
||
168 | $syncStatus = $this->consistencyCheck( $relevantPaths ); |
||
169 | if ( !$syncStatus->isOK() ) { |
||
170 | wfDebugLog( 'FileOperation', get_class( $this ) . |
||
171 | " failed sync check: " . FormatJson::encode( $relevantPaths ) ); |
||
172 | // Try to resync the clone backends to the master on the spot... |
||
173 | if ( $this->autoResync === false |
||
174 | || !$this->resyncFiles( $relevantPaths, $this->autoResync )->isOK() |
||
175 | ) { |
||
176 | $status->merge( $syncStatus ); |
||
177 | |||
178 | return $status; // abort |
||
179 | } |
||
180 | } |
||
181 | // Actually attempt the operation batch on the master backend... |
||
182 | $realOps = $this->substOpBatchPaths( $ops, $mbe ); |
||
183 | $masterStatus = $mbe->doOperations( $realOps, $opts ); |
||
184 | $status->merge( $masterStatus ); |
||
185 | // Propagate the operations to the clone backends if there were no unexpected errors |
||
186 | // and if there were either no expected errors or if the 'force' option was used. |
||
187 | // However, if nothing succeeded at all, then don't replicate any of the operations. |
||
188 | // If $ops only had one operation, this might avoid backend sync inconsistencies. |
||
189 | if ( $masterStatus->isOK() && $masterStatus->successCount > 0 ) { |
||
190 | foreach ( $this->backends as $index => $backend ) { |
||
191 | if ( $index === $this->masterIndex ) { |
||
192 | continue; // done already |
||
193 | } |
||
194 | |||
195 | $realOps = $this->substOpBatchPaths( $ops, $backend ); |
||
196 | if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) { |
||
197 | // Bind $scopeLock to the callback to preserve locks |
||
198 | DeferredUpdates::addCallableUpdate( |
||
199 | function() use ( $backend, $realOps, $opts, $scopeLock, $relevantPaths ) { |
||
200 | wfDebugLog( 'FileOperationReplication', |
||
201 | "'{$backend->getName()}' async replication; paths: " . |
||
202 | FormatJson::encode( $relevantPaths ) ); |
||
203 | $backend->doOperations( $realOps, $opts ); |
||
204 | } |
||
205 | ); |
||
206 | } else { |
||
207 | wfDebugLog( 'FileOperationReplication', |
||
208 | "'{$backend->getName()}' sync replication; paths: " . |
||
209 | FormatJson::encode( $relevantPaths ) ); |
||
210 | $status->merge( $backend->doOperations( $realOps, $opts ) ); |
||
211 | } |
||
212 | } |
||
213 | } |
||
214 | // Make 'success', 'successCount', and 'failCount' fields reflect |
||
215 | // the overall operation, rather than all the batches for each backend. |
||
216 | // Do this by only using success values from the master backend's batch. |
||
217 | $status->success = $masterStatus->success; |
||
218 | $status->successCount = $masterStatus->successCount; |
||
219 | $status->failCount = $masterStatus->failCount; |
||
220 | |||
221 | return $status; |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * Check that a set of files are consistent across all internal backends |
||
226 | * |
||
227 | * @param array $paths List of storage paths |
||
228 | * @return StatusValue |
||
229 | */ |
||
230 | public function consistencyCheck( array $paths ) { |
||
231 | $status = $this->newStatus(); |
||
232 | if ( $this->syncChecks == 0 || count( $this->backends ) <= 1 ) { |
||
233 | return $status; // skip checks |
||
234 | } |
||
235 | |||
236 | // Preload all of the stat info in as few round trips as possible... |
||
237 | foreach ( $this->backends as $backend ) { |
||
238 | $realPaths = $this->substPaths( $paths, $backend ); |
||
239 | $backend->preloadFileStat( [ 'srcs' => $realPaths, 'latest' => true ] ); |
||
240 | } |
||
241 | |||
242 | $mBackend = $this->backends[$this->masterIndex]; |
||
243 | foreach ( $paths as $path ) { |
||
244 | $params = [ 'src' => $path, 'latest' => true ]; |
||
245 | $mParams = $this->substOpPaths( $params, $mBackend ); |
||
246 | // Stat the file on the 'master' backend |
||
247 | $mStat = $mBackend->getFileStat( $mParams ); |
||
248 | if ( $this->syncChecks & self::CHECK_SHA1 ) { |
||
249 | $mSha1 = $mBackend->getFileSha1Base36( $mParams ); |
||
250 | } else { |
||
251 | $mSha1 = false; |
||
252 | } |
||
253 | // Check if all clone backends agree with the master... |
||
254 | foreach ( $this->backends as $index => $cBackend ) { |
||
255 | if ( $index === $this->masterIndex ) { |
||
256 | continue; // master |
||
257 | } |
||
258 | $cParams = $this->substOpPaths( $params, $cBackend ); |
||
259 | $cStat = $cBackend->getFileStat( $cParams ); |
||
260 | if ( $mStat ) { // file is in master |
||
261 | if ( !$cStat ) { // file should exist |
||
262 | $status->fatal( 'backend-fail-synced', $path ); |
||
263 | continue; |
||
264 | } |
||
265 | if ( $this->syncChecks & self::CHECK_SIZE ) { |
||
266 | if ( $cStat['size'] != $mStat['size'] ) { // wrong size |
||
267 | $status->fatal( 'backend-fail-synced', $path ); |
||
268 | continue; |
||
269 | } |
||
270 | } |
||
271 | if ( $this->syncChecks & self::CHECK_TIME ) { |
||
272 | $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] ); |
||
273 | $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] ); |
||
274 | if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere |
||
275 | $status->fatal( 'backend-fail-synced', $path ); |
||
276 | continue; |
||
277 | } |
||
278 | } |
||
279 | if ( $this->syncChecks & self::CHECK_SHA1 ) { |
||
280 | if ( $cBackend->getFileSha1Base36( $cParams ) !== $mSha1 ) { // wrong SHA1 |
||
281 | $status->fatal( 'backend-fail-synced', $path ); |
||
282 | continue; |
||
283 | } |
||
284 | } |
||
285 | } else { // file is not in master |
||
286 | if ( $cStat ) { // file should not exist |
||
287 | $status->fatal( 'backend-fail-synced', $path ); |
||
288 | } |
||
289 | } |
||
290 | } |
||
291 | } |
||
292 | |||
293 | return $status; |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Check that a set of file paths are usable across all internal backends |
||
298 | * |
||
299 | * @param array $paths List of storage paths |
||
300 | * @return StatusValue |
||
301 | */ |
||
302 | public function accessibilityCheck( array $paths ) { |
||
303 | $status = $this->newStatus(); |
||
304 | if ( count( $this->backends ) <= 1 ) { |
||
305 | return $status; // skip checks |
||
306 | } |
||
307 | |||
308 | foreach ( $paths as $path ) { |
||
309 | foreach ( $this->backends as $backend ) { |
||
310 | $realPath = $this->substPaths( $path, $backend ); |
||
311 | if ( !$backend->isPathUsableInternal( $realPath ) ) { |
||
0 ignored issues
–
show
It seems like
$realPath defined by $this->substPaths($path, $backend) on line 310 can also be of type array<integer,string> ; however, FileBackendStore::isPathUsableInternal() does only seem to accept string , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
312 | $status->fatal( 'backend-fail-usable', $path ); |
||
313 | } |
||
314 | } |
||
315 | } |
||
316 | |||
317 | return $status; |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * Check that a set of files are consistent across all internal backends |
||
322 | * and re-synchronize those files against the "multi master" if needed. |
||
323 | * |
||
324 | * @param array $paths List of storage paths |
||
325 | * @param string|bool $resyncMode False, True, or "conservative"; see __construct() |
||
326 | * @return StatusValue |
||
327 | */ |
||
328 | public function resyncFiles( array $paths, $resyncMode = true ) { |
||
329 | $status = $this->newStatus(); |
||
330 | |||
331 | $mBackend = $this->backends[$this->masterIndex]; |
||
332 | foreach ( $paths as $path ) { |
||
333 | $mPath = $this->substPaths( $path, $mBackend ); |
||
334 | $mSha1 = $mBackend->getFileSha1Base36( [ 'src' => $mPath, 'latest' => true ] ); |
||
335 | $mStat = $mBackend->getFileStat( [ 'src' => $mPath, 'latest' => true ] ); |
||
336 | View Code Duplication | if ( $mStat === null || ( $mSha1 !== false && !$mStat ) ) { // sanity |
|
337 | $status->fatal( 'backend-fail-internal', $this->name ); |
||
338 | wfDebugLog( 'FileOperation', __METHOD__ |
||
339 | . ': File is not available on the master backend' ); |
||
340 | continue; // file is not available on the master backend... |
||
341 | } |
||
342 | // Check of all clone backends agree with the master... |
||
343 | foreach ( $this->backends as $index => $cBackend ) { |
||
344 | if ( $index === $this->masterIndex ) { |
||
345 | continue; // master |
||
346 | } |
||
347 | $cPath = $this->substPaths( $path, $cBackend ); |
||
348 | $cSha1 = $cBackend->getFileSha1Base36( [ 'src' => $cPath, 'latest' => true ] ); |
||
349 | $cStat = $cBackend->getFileStat( [ 'src' => $cPath, 'latest' => true ] ); |
||
350 | View Code Duplication | if ( $cStat === null || ( $cSha1 !== false && !$cStat ) ) { // sanity |
|
351 | $status->fatal( 'backend-fail-internal', $cBackend->getName() ); |
||
352 | wfDebugLog( 'FileOperation', __METHOD__ . |
||
353 | ': File is not available on the clone backend' ); |
||
354 | continue; // file is not available on the clone backend... |
||
355 | } |
||
356 | if ( $mSha1 === $cSha1 ) { |
||
0 ignored issues
–
show
This
if statement is empty and can be removed.
This check looks for the bodies of These if (rand(1, 6) > 3) {
//print "Check failed";
} else {
print "Check succeeded";
}
could be turned into if (rand(1, 6) <= 3) {
print "Check succeeded";
}
This is much more concise to read. ![]() |
|||
357 | // already synced; nothing to do |
||
358 | } elseif ( $mSha1 !== false ) { // file is in master |
||
359 | if ( $resyncMode === 'conservative' |
||
360 | && $cStat && $cStat['mtime'] > $mStat['mtime'] |
||
361 | ) { |
||
362 | $status->fatal( 'backend-fail-synced', $path ); |
||
363 | continue; // don't rollback data |
||
364 | } |
||
365 | $fsFile = $mBackend->getLocalReference( |
||
366 | [ 'src' => $mPath, 'latest' => true ] ); |
||
367 | $status->merge( $cBackend->quickStore( |
||
368 | [ 'src' => $fsFile->getPath(), 'dst' => $cPath ] |
||
369 | ) ); |
||
370 | } elseif ( $mStat === false ) { // file is not in master |
||
371 | if ( $resyncMode === 'conservative' ) { |
||
372 | $status->fatal( 'backend-fail-synced', $path ); |
||
373 | continue; // don't delete data |
||
374 | } |
||
375 | $status->merge( $cBackend->quickDelete( [ 'src' => $cPath ] ) ); |
||
376 | } |
||
377 | } |
||
378 | } |
||
379 | |||
380 | if ( !$status->isOK() ) { |
||
381 | wfDebugLog( 'FileOperation', get_class( $this ) . |
||
382 | " failed to resync: " . FormatJson::encode( $paths ) ); |
||
383 | } |
||
384 | |||
385 | return $status; |
||
386 | } |
||
387 | |||
388 | /** |
||
389 | * Get a list of file storage paths to read or write for a list of operations |
||
390 | * |
||
391 | * @param array $ops Same format as doOperations() |
||
392 | * @return array List of storage paths to files (does not include directories) |
||
393 | */ |
||
394 | protected function fileStoragePathsForOps( array $ops ) { |
||
395 | $paths = []; |
||
396 | foreach ( $ops as $op ) { |
||
397 | if ( isset( $op['src'] ) ) { |
||
398 | // For things like copy/move/delete with "ignoreMissingSource" and there |
||
399 | // is no source file, nothing should happen and there should be no errors. |
||
400 | if ( empty( $op['ignoreMissingSource'] ) |
||
401 | || $this->fileExists( [ 'src' => $op['src'] ] ) |
||
402 | ) { |
||
403 | $paths[] = $op['src']; |
||
404 | } |
||
405 | } |
||
406 | if ( isset( $op['srcs'] ) ) { |
||
407 | $paths = array_merge( $paths, $op['srcs'] ); |
||
408 | } |
||
409 | if ( isset( $op['dst'] ) ) { |
||
410 | $paths[] = $op['dst']; |
||
411 | } |
||
412 | } |
||
413 | |||
414 | return array_values( array_unique( array_filter( $paths, 'FileBackend::isStoragePath' ) ) ); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Substitute the backend name in storage path parameters |
||
419 | * for a set of operations with that of a given internal backend. |
||
420 | * |
||
421 | * @param array $ops List of file operation arrays |
||
422 | * @param FileBackendStore $backend |
||
423 | * @return array |
||
424 | */ |
||
425 | protected function substOpBatchPaths( array $ops, FileBackendStore $backend ) { |
||
426 | $newOps = []; // operations |
||
427 | foreach ( $ops as $op ) { |
||
428 | $newOp = $op; // operation |
||
429 | foreach ( [ 'src', 'srcs', 'dst', 'dir' ] as $par ) { |
||
430 | if ( isset( $newOp[$par] ) ) { // string or array |
||
431 | $newOp[$par] = $this->substPaths( $newOp[$par], $backend ); |
||
432 | } |
||
433 | } |
||
434 | $newOps[] = $newOp; |
||
435 | } |
||
436 | |||
437 | return $newOps; |
||
438 | } |
||
439 | |||
440 | /** |
||
441 | * Same as substOpBatchPaths() but for a single operation |
||
442 | * |
||
443 | * @param array $ops File operation array |
||
444 | * @param FileBackendStore $backend |
||
445 | * @return array |
||
446 | */ |
||
447 | protected function substOpPaths( array $ops, FileBackendStore $backend ) { |
||
448 | $newOps = $this->substOpBatchPaths( [ $ops ], $backend ); |
||
449 | |||
450 | return $newOps[0]; |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | * Substitute the backend of storage paths with an internal backend's name |
||
455 | * |
||
456 | * @param array|string $paths List of paths or single string path |
||
457 | * @param FileBackendStore $backend |
||
458 | * @return array|string |
||
459 | */ |
||
460 | protected function substPaths( $paths, FileBackendStore $backend ) { |
||
461 | return preg_replace( |
||
462 | '!^mwstore://' . preg_quote( $this->name, '!' ) . '/!', |
||
463 | StringUtils::escapeRegexReplacement( "mwstore://{$backend->getName()}/" ), |
||
464 | $paths // string or array |
||
465 | ); |
||
466 | } |
||
467 | |||
468 | /** |
||
469 | * Substitute the backend of internal storage paths with the proxy backend's name |
||
470 | * |
||
471 | * @param array|string $paths List of paths or single string path |
||
472 | * @return array|string |
||
473 | */ |
||
474 | protected function unsubstPaths( $paths ) { |
||
475 | return preg_replace( |
||
476 | '!^mwstore://([^/]+)!', |
||
477 | StringUtils::escapeRegexReplacement( "mwstore://{$this->name}" ), |
||
478 | $paths // string or array |
||
479 | ); |
||
480 | } |
||
481 | |||
482 | /** |
||
483 | * @param array $ops File operations for FileBackend::doOperations() |
||
484 | * @return bool Whether there are file path sources with outside lifetime/ownership |
||
485 | */ |
||
486 | protected function hasVolatileSources( array $ops ) { |
||
487 | foreach ( $ops as $op ) { |
||
488 | if ( $op['op'] === 'store' && !isset( $op['srcRef'] ) ) { |
||
489 | return true; // source file might be deleted anytime after do*Operations() |
||
490 | } |
||
491 | } |
||
492 | |||
493 | return false; |
||
494 | } |
||
495 | |||
496 | protected function doQuickOperationsInternal( array $ops ) { |
||
497 | $status = $this->newStatus(); |
||
498 | // Do the operations on the master backend; setting StatusValue fields... |
||
499 | $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); |
||
500 | $masterStatus = $this->backends[$this->masterIndex]->doQuickOperations( $realOps ); |
||
501 | $status->merge( $masterStatus ); |
||
502 | // Propagate the operations to the clone backends... |
||
503 | View Code Duplication | foreach ( $this->backends as $index => $backend ) { |
|
504 | if ( $index === $this->masterIndex ) { |
||
505 | continue; // done already |
||
506 | } |
||
507 | |||
508 | $realOps = $this->substOpBatchPaths( $ops, $backend ); |
||
509 | if ( $this->asyncWrites && !$this->hasVolatileSources( $ops ) ) { |
||
510 | DeferredUpdates::addCallableUpdate( |
||
511 | function() use ( $backend, $realOps ) { |
||
512 | $backend->doQuickOperations( $realOps ); |
||
513 | } |
||
514 | ); |
||
515 | } else { |
||
516 | $status->merge( $backend->doQuickOperations( $realOps ) ); |
||
517 | } |
||
518 | } |
||
519 | // Make 'success', 'successCount', and 'failCount' fields reflect |
||
520 | // the overall operation, rather than all the batches for each backend. |
||
521 | // Do this by only using success values from the master backend's batch. |
||
522 | $status->success = $masterStatus->success; |
||
523 | $status->successCount = $masterStatus->successCount; |
||
524 | $status->failCount = $masterStatus->failCount; |
||
525 | |||
526 | return $status; |
||
527 | } |
||
528 | |||
529 | protected function doPrepare( array $params ) { |
||
530 | return $this->doDirectoryOp( 'prepare', $params ); |
||
531 | } |
||
532 | |||
533 | protected function doSecure( array $params ) { |
||
534 | return $this->doDirectoryOp( 'secure', $params ); |
||
535 | } |
||
536 | |||
537 | protected function doPublish( array $params ) { |
||
538 | return $this->doDirectoryOp( 'publish', $params ); |
||
539 | } |
||
540 | |||
541 | protected function doClean( array $params ) { |
||
542 | return $this->doDirectoryOp( 'clean', $params ); |
||
543 | } |
||
544 | |||
545 | /** |
||
546 | * @param string $method One of (doPrepare,doSecure,doPublish,doClean) |
||
547 | * @param array $params Method arguments |
||
548 | * @return StatusValue |
||
549 | */ |
||
550 | protected function doDirectoryOp( $method, array $params ) { |
||
551 | $status = $this->newStatus(); |
||
552 | |||
553 | $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); |
||
554 | $masterStatus = $this->backends[$this->masterIndex]->$method( $realParams ); |
||
555 | $status->merge( $masterStatus ); |
||
556 | |||
557 | View Code Duplication | foreach ( $this->backends as $index => $backend ) { |
|
558 | if ( $index === $this->masterIndex ) { |
||
559 | continue; // already done |
||
560 | } |
||
561 | |||
562 | $realParams = $this->substOpPaths( $params, $backend ); |
||
563 | if ( $this->asyncWrites ) { |
||
564 | DeferredUpdates::addCallableUpdate( |
||
565 | function() use ( $backend, $method, $realParams ) { |
||
566 | $backend->$method( $realParams ); |
||
567 | } |
||
568 | ); |
||
569 | } else { |
||
570 | $status->merge( $backend->$method( $realParams ) ); |
||
571 | } |
||
572 | } |
||
573 | |||
574 | return $status; |
||
575 | } |
||
576 | |||
577 | public function concatenate( array $params ) { |
||
578 | $status = $this->newStatus(); |
||
579 | // We are writing to an FS file, so we don't need to do this per-backend |
||
580 | $index = $this->getReadIndexFromParams( $params ); |
||
581 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
582 | |||
583 | $status->merge( $this->backends[$index]->concatenate( $realParams ) ); |
||
584 | |||
585 | return $status; |
||
586 | } |
||
587 | |||
588 | public function fileExists( array $params ) { |
||
589 | $index = $this->getReadIndexFromParams( $params ); |
||
590 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
591 | |||
592 | return $this->backends[$index]->fileExists( $realParams ); |
||
593 | } |
||
594 | |||
595 | public function getFileTimestamp( array $params ) { |
||
596 | $index = $this->getReadIndexFromParams( $params ); |
||
597 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
598 | |||
599 | return $this->backends[$index]->getFileTimestamp( $realParams ); |
||
600 | } |
||
601 | |||
602 | public function getFileSize( array $params ) { |
||
603 | $index = $this->getReadIndexFromParams( $params ); |
||
604 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
605 | |||
606 | return $this->backends[$index]->getFileSize( $realParams ); |
||
607 | } |
||
608 | |||
609 | public function getFileStat( array $params ) { |
||
610 | $index = $this->getReadIndexFromParams( $params ); |
||
611 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
612 | |||
613 | return $this->backends[$index]->getFileStat( $realParams ); |
||
614 | } |
||
615 | |||
616 | public function getFileXAttributes( array $params ) { |
||
617 | $index = $this->getReadIndexFromParams( $params ); |
||
618 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
619 | |||
620 | return $this->backends[$index]->getFileXAttributes( $realParams ); |
||
621 | } |
||
622 | |||
623 | View Code Duplication | public function getFileContentsMulti( array $params ) { |
|
624 | $index = $this->getReadIndexFromParams( $params ); |
||
625 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
626 | |||
627 | $contentsM = $this->backends[$index]->getFileContentsMulti( $realParams ); |
||
628 | |||
629 | $contents = []; // (path => FSFile) mapping using the proxy backend's name |
||
630 | foreach ( $contentsM as $path => $data ) { |
||
631 | $contents[$this->unsubstPaths( $path )] = $data; |
||
632 | } |
||
633 | |||
634 | return $contents; |
||
635 | } |
||
636 | |||
637 | public function getFileSha1Base36( array $params ) { |
||
638 | $index = $this->getReadIndexFromParams( $params ); |
||
639 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
640 | |||
641 | return $this->backends[$index]->getFileSha1Base36( $realParams ); |
||
642 | } |
||
643 | |||
644 | public function getFileProps( array $params ) { |
||
645 | $index = $this->getReadIndexFromParams( $params ); |
||
646 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
647 | |||
648 | return $this->backends[$index]->getFileProps( $realParams ); |
||
649 | } |
||
650 | |||
651 | public function streamFile( array $params ) { |
||
652 | $index = $this->getReadIndexFromParams( $params ); |
||
653 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
654 | |||
655 | return $this->backends[$index]->streamFile( $realParams ); |
||
656 | } |
||
657 | |||
658 | View Code Duplication | public function getLocalReferenceMulti( array $params ) { |
|
659 | $index = $this->getReadIndexFromParams( $params ); |
||
660 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
661 | |||
662 | $fsFilesM = $this->backends[$index]->getLocalReferenceMulti( $realParams ); |
||
663 | |||
664 | $fsFiles = []; // (path => FSFile) mapping using the proxy backend's name |
||
665 | foreach ( $fsFilesM as $path => $fsFile ) { |
||
666 | $fsFiles[$this->unsubstPaths( $path )] = $fsFile; |
||
667 | } |
||
668 | |||
669 | return $fsFiles; |
||
670 | } |
||
671 | |||
672 | View Code Duplication | public function getLocalCopyMulti( array $params ) { |
|
673 | $index = $this->getReadIndexFromParams( $params ); |
||
674 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
675 | |||
676 | $tempFilesM = $this->backends[$index]->getLocalCopyMulti( $realParams ); |
||
677 | |||
678 | $tempFiles = []; // (path => TempFSFile) mapping using the proxy backend's name |
||
679 | foreach ( $tempFilesM as $path => $tempFile ) { |
||
680 | $tempFiles[$this->unsubstPaths( $path )] = $tempFile; |
||
681 | } |
||
682 | |||
683 | return $tempFiles; |
||
684 | } |
||
685 | |||
686 | public function getFileHttpUrl( array $params ) { |
||
687 | $index = $this->getReadIndexFromParams( $params ); |
||
688 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
689 | |||
690 | return $this->backends[$index]->getFileHttpUrl( $realParams ); |
||
691 | } |
||
692 | |||
693 | public function directoryExists( array $params ) { |
||
694 | $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); |
||
695 | |||
696 | return $this->backends[$this->masterIndex]->directoryExists( $realParams ); |
||
697 | } |
||
698 | |||
699 | public function getDirectoryList( array $params ) { |
||
700 | $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); |
||
701 | |||
702 | return $this->backends[$this->masterIndex]->getDirectoryList( $realParams ); |
||
703 | } |
||
704 | |||
705 | public function getFileList( array $params ) { |
||
706 | $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] ); |
||
707 | |||
708 | return $this->backends[$this->masterIndex]->getFileList( $realParams ); |
||
709 | } |
||
710 | |||
711 | public function getFeatures() { |
||
712 | return $this->backends[$this->masterIndex]->getFeatures(); |
||
713 | } |
||
714 | |||
715 | public function clearCache( array $paths = null ) { |
||
716 | foreach ( $this->backends as $backend ) { |
||
717 | $realPaths = is_array( $paths ) ? $this->substPaths( $paths, $backend ) : null; |
||
718 | $backend->clearCache( $realPaths ); |
||
0 ignored issues
–
show
It seems like
$realPaths defined by is_array($paths) ? $this...paths, $backend) : null on line 717 can also be of type string ; however, FileBackendStore::clearCache() does only seem to accept null|array , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
719 | } |
||
720 | } |
||
721 | |||
722 | public function preloadCache( array $paths ) { |
||
723 | $realPaths = $this->substPaths( $paths, $this->backends[$this->readIndex] ); |
||
724 | $this->backends[$this->readIndex]->preloadCache( $realPaths ); |
||
0 ignored issues
–
show
It seems like
$realPaths defined by $this->substPaths($paths...ends[$this->readIndex]) on line 723 can also be of type string ; however, FileBackendStore::preloadCache() does only seem to accept array , maybe add an additional type check?
If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check: /**
* @return array|string
*/
function returnsDifferentValues($x) {
if ($x) {
return 'foo';
}
return array();
}
$x = returnsDifferentValues($y);
if (is_array($x)) {
// $x is an array.
}
If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue. ![]() |
|||
725 | } |
||
726 | |||
727 | public function preloadFileStat( array $params ) { |
||
728 | $index = $this->getReadIndexFromParams( $params ); |
||
729 | $realParams = $this->substOpPaths( $params, $this->backends[$index] ); |
||
730 | |||
731 | return $this->backends[$index]->preloadFileStat( $realParams ); |
||
732 | } |
||
733 | |||
734 | public function getScopedLocksForOps( array $ops, StatusValue $status ) { |
||
735 | $realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] ); |
||
736 | $fileOps = $this->backends[$this->masterIndex]->getOperationsInternal( $realOps ); |
||
737 | // Get the paths to lock from the master backend |
||
738 | $paths = $this->backends[$this->masterIndex]->getPathsToLockForOpsInternal( $fileOps ); |
||
739 | // Get the paths under the proxy backend's name |
||
740 | $pbPaths = [ |
||
741 | LockManager::LOCK_UW => $this->unsubstPaths( $paths[LockManager::LOCK_UW] ), |
||
742 | LockManager::LOCK_EX => $this->unsubstPaths( $paths[LockManager::LOCK_EX] ) |
||
743 | ]; |
||
744 | |||
745 | // Actually acquire the locks |
||
746 | return $this->getScopedFileLocks( $pbPaths, 'mixed', $status ); |
||
747 | } |
||
748 | |||
749 | /** |
||
750 | * @param array $params |
||
751 | * @return int The master or read affinity backend index, based on $params['latest'] |
||
752 | */ |
||
753 | protected function getReadIndexFromParams( array $params ) { |
||
754 | return !empty( $params['latest'] ) ? $this->masterIndex : $this->readIndex; |
||
755 | } |
||
756 | } |
||
757 |
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 theid
property of an instance of theAccount
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.