Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

libs/filebackend/FileBackendMultiWrite.php (7 issues)

Upgrade to new PHP Analysis Engine

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
Documentation Bug introduced by
It seems like $index can also be of type string. However, the property $masterIndex 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 $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...
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
Documentation Bug introduced by
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 $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...
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
Documentation Bug introduced by
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 $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...
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.

Loading history...
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 if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

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.

Loading history...
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.

Loading history...
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.

Loading history...
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