SwiftFileBackendDirList::pageFromList()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 5
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * OpenStack Swift based file backend.
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 Russ Nelson
23
 * @author Aaron Schulz
24
 */
25
26
/**
27
 * @brief Class for an OpenStack Swift (or Ceph RGW) based file backend.
28
 *
29
 * StatusValue messages should avoid mentioning the Swift account name.
30
 * Likewise, error suppression should be used to avoid path disclosure.
31
 *
32
 * @ingroup FileBackend
33
 * @since 1.19
34
 */
35
class SwiftFileBackend extends FileBackendStore {
36
	/** @var MultiHttpClient */
37
	protected $http;
38
39
	/** @var int TTL in seconds */
40
	protected $authTTL;
41
42
	/** @var string Authentication base URL (without version) */
43
	protected $swiftAuthUrl;
44
45
	/** @var string Swift user (account:user) to authenticate as */
46
	protected $swiftUser;
47
48
	/** @var string Secret key for user */
49
	protected $swiftKey;
50
51
	/** @var string Shared secret value for making temp URLs */
52
	protected $swiftTempUrlKey;
53
54
	/** @var string S3 access key (RADOS Gateway) */
55
	protected $rgwS3AccessKey;
56
57
	/** @var string S3 authentication key (RADOS Gateway) */
58
	protected $rgwS3SecretKey;
59
60
	/** @var BagOStuff */
61
	protected $srvCache;
62
63
	/** @var ProcessCacheLRU Container stat cache */
64
	protected $containerStatCache;
65
66
	/** @var array */
67
	protected $authCreds;
68
69
	/** @var int UNIX timestamp */
70
	protected $authSessionTimestamp = 0;
71
72
	/** @var int UNIX timestamp */
73
	protected $authErrorTimestamp = null;
74
75
	/** @var bool Whether the server is an Ceph RGW */
76
	protected $isRGW = false;
77
78
	/**
79
	 * @see FileBackendStore::__construct()
80
	 * Additional $config params include:
81
	 *   - swiftAuthUrl       : Swift authentication server URL
82
	 *   - swiftUser          : Swift user used by MediaWiki (account:username)
83
	 *   - swiftKey           : Swift authentication key for the above user
84
	 *   - swiftAuthTTL       : Swift authentication TTL (seconds)
85
	 *   - swiftTempUrlKey    : Swift "X-Account-Meta-Temp-URL-Key" value on the account.
86
	 *                          Do not set this until it has been set in the backend.
87
	 *   - shardViaHashLevels : Map of container names to sharding config with:
88
	 *                             - base   : base of hash characters, 16 or 36
89
	 *                             - levels : the number of hash levels (and digits)
90
	 *                             - repeat : hash subdirectories are prefixed with all the
91
	 *                                        parent hash directory names (e.g. "a/ab/abc")
92
	 *   - cacheAuthInfo      : Whether to cache authentication tokens in APC, XCache, ect.
93
	 *                          If those are not available, then the main cache will be used.
94
	 *                          This is probably insecure in shared hosting environments.
95
	 *   - rgwS3AccessKey     : Rados Gateway S3 "access key" value on the account.
96
	 *                          Do not set this until it has been set in the backend.
97
	 *                          This is used for generating expiring pre-authenticated URLs.
98
	 *                          Only use this when using rgw and to work around
99
	 *                          http://tracker.newdream.net/issues/3454.
100
	 *   - rgwS3SecretKey     : Rados Gateway S3 "secret key" value on the account.
101
	 *                          Do not set this until it has been set in the backend.
102
	 *                          This is used for generating expiring pre-authenticated URLs.
103
	 *                          Only use this when using rgw and to work around
104
	 *                          http://tracker.newdream.net/issues/3454.
105
	 */
106
	public function __construct( array $config ) {
107
		parent::__construct( $config );
108
		// Required settings
109
		$this->swiftAuthUrl = $config['swiftAuthUrl'];
110
		$this->swiftUser = $config['swiftUser'];
111
		$this->swiftKey = $config['swiftKey'];
112
		// Optional settings
113
		$this->authTTL = isset( $config['swiftAuthTTL'] )
114
			? $config['swiftAuthTTL']
115
			: 15 * 60; // some sane number
116
		$this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
117
			? $config['swiftTempUrlKey']
118
			: '';
119
		$this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
120
			? $config['shardViaHashLevels']
121
			: '';
122
		$this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
123
			? $config['rgwS3AccessKey']
124
			: '';
125
		$this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
126
			? $config['rgwS3SecretKey']
127
			: '';
128
		// HTTP helper client
129
		$this->http = new MultiHttpClient( [] );
130
		// Cache container information to mask latency
131
		if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
132
			$this->memCache = $config['wanCache'];
133
		}
134
		// Process cache for container info
135
		$this->containerStatCache = new ProcessCacheLRU( 300 );
136
		// Cache auth token information to avoid RTTs
137
		if ( !empty( $config['cacheAuthInfo'] ) && isset( $config['srvCache'] ) ) {
138
			$this->srvCache = $config['srvCache'];
139
		} else {
140
			$this->srvCache = new EmptyBagOStuff();
141
		}
142
	}
143
144
	public function getFeatures() {
145
		return ( FileBackend::ATTR_UNICODE_PATHS |
146
			FileBackend::ATTR_HEADERS | FileBackend::ATTR_METADATA );
147
	}
148
149
	protected function resolveContainerPath( $container, $relStoragePath ) {
150
		if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
151
			return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
152
		} elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
153
			return null; // too long for Swift
154
		}
155
156
		return $relStoragePath;
157
	}
158
159
	public function isPathUsableInternal( $storagePath ) {
160
		list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
161
		if ( $rel === null ) {
162
			return false; // invalid
163
		}
164
165
		return is_array( $this->getContainerStat( $container ) );
166
	}
167
168
	/**
169
	 * Sanitize and filter the custom headers from a $params array.
170
	 * Only allows certain "standard" Content- and X-Content- headers.
171
	 *
172
	 * @param array $params
173
	 * @return array Sanitized value of 'headers' field in $params
174
	 */
175
	protected function sanitizeHdrs( array $params ) {
176
		return isset( $params['headers'] )
177
			? $this->getCustomHeaders( $params['headers'] )
178
			: [];
179
	}
180
181
	/**
182
	 * @param array $rawHeaders
183
	 * @return array Custom non-metadata HTTP headers
184
	 */
185
	protected function getCustomHeaders( array $rawHeaders ) {
186
		$headers = [];
187
188
		// Normalize casing, and strip out illegal headers
189
		foreach ( $rawHeaders as $name => $value ) {
190
			$name = strtolower( $name );
191
			if ( preg_match( '/^content-(type|length)$/', $name ) ) {
192
				continue; // blacklisted
193
			} elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
194
				$headers[$name] = $value; // allowed
195
			} elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
196
				$headers[$name] = $value; // allowed
197
			}
198
		}
199
		// By default, Swift has annoyingly low maximum header value limits
200
		if ( isset( $headers['content-disposition'] ) ) {
201
			$disposition = '';
202
			// @note: assume FileBackend::makeContentDisposition() already used
203
			foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
204
				$part = trim( $part );
205
				$new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
206
				if ( strlen( $new ) <= 255 ) {
207
					$disposition = $new;
208
				} else {
209
					break; // too long; sigh
210
				}
211
			}
212
			$headers['content-disposition'] = $disposition;
213
		}
214
215
		return $headers;
216
	}
217
218
	/**
219
	 * @param array $rawHeaders
220
	 * @return array Custom metadata headers
221
	 */
222
	protected function getMetadataHeaders( array $rawHeaders ) {
223
		$headers = [];
224
		foreach ( $rawHeaders as $name => $value ) {
225
			$name = strtolower( $name );
226
			if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
227
				$headers[$name] = $value;
228
			}
229
		}
230
231
		return $headers;
232
	}
233
234
	/**
235
	 * @param array $rawHeaders
236
	 * @return array Custom metadata headers with prefix removed
237
	 */
238
	protected function getMetadata( array $rawHeaders ) {
239
		$metadata = [];
240
		foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
241
			$metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
242
		}
243
244
		return $metadata;
245
	}
246
247
	protected function doCreateInternal( array $params ) {
248
		$status = $this->newStatus();
249
250
		list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
251
		if ( $dstRel === null ) {
252
			$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
253
254
			return $status;
255
		}
256
257
		$sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
258
		$contentType = isset( $params['headers']['content-type'] )
259
			? $params['headers']['content-type']
260
			: $this->getContentType( $params['dst'], $params['content'], null );
261
262
		$reqs = [ [
263
			'method' => 'PUT',
264
			'url' => [ $dstCont, $dstRel ],
265
			'headers' => [
266
				'content-length' => strlen( $params['content'] ),
267
				'etag' => md5( $params['content'] ),
268
				'content-type' => $contentType,
269
				'x-object-meta-sha1base36' => $sha1Hash
270
			] + $this->sanitizeHdrs( $params ),
271
			'body' => $params['content']
272
		] ];
273
274
		$method = __METHOD__;
275 View Code Duplication
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
276
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
277
			if ( $rcode === 201 ) {
0 ignored issues
show
Unused Code introduced by
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...
278
				// good
279
			} elseif ( $rcode === 412 ) {
280
				$status->fatal( 'backend-fail-contenttype', $params['dst'] );
281
			} else {
282
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
283
			}
284
		};
285
286
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
287 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
288
			$status->value = $opHandle;
289
		} else { // actually write the object in Swift
290
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
291
		}
292
293
		return $status;
294
	}
295
296
	protected function doStoreInternal( array $params ) {
297
		$status = $this->newStatus();
298
299
		list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
300
		if ( $dstRel === null ) {
301
			$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
302
303
			return $status;
304
		}
305
306
		MediaWiki\suppressWarnings();
307
		$sha1Hash = sha1_file( $params['src'] );
308
		MediaWiki\restoreWarnings();
309 View Code Duplication
		if ( $sha1Hash === false ) { // source doesn't exist?
310
			$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
311
312
			return $status;
313
		}
314
		$sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
315
		$contentType = isset( $params['headers']['content-type'] )
316
			? $params['headers']['content-type']
317
			: $this->getContentType( $params['dst'], null, $params['src'] );
318
319
		$handle = fopen( $params['src'], 'rb' );
320 View Code Duplication
		if ( $handle === false ) { // source doesn't exist?
321
			$status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
322
323
			return $status;
324
		}
325
326
		$reqs = [ [
327
			'method' => 'PUT',
328
			'url' => [ $dstCont, $dstRel ],
329
			'headers' => [
330
				'content-length' => filesize( $params['src'] ),
331
				'etag' => md5_file( $params['src'] ),
332
				'content-type' => $contentType,
333
				'x-object-meta-sha1base36' => $sha1Hash
334
			] + $this->sanitizeHdrs( $params ),
335
			'body' => $handle // resource
336
		] ];
337
338
		$method = __METHOD__;
339 View Code Duplication
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
340
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
341
			if ( $rcode === 201 ) {
0 ignored issues
show
Unused Code introduced by
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...
342
				// good
343
			} elseif ( $rcode === 412 ) {
344
				$status->fatal( 'backend-fail-contenttype', $params['dst'] );
345
			} else {
346
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
347
			}
348
		};
349
350
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
351 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
352
			$status->value = $opHandle;
353
		} else { // actually write the object in Swift
354
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
355
		}
356
357
		return $status;
358
	}
359
360
	protected function doCopyInternal( array $params ) {
361
		$status = $this->newStatus();
362
363
		list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
364
		if ( $srcRel === null ) {
365
			$status->fatal( 'backend-fail-invalidpath', $params['src'] );
366
367
			return $status;
368
		}
369
370
		list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
371
		if ( $dstRel === null ) {
372
			$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
373
374
			return $status;
375
		}
376
377
		$reqs = [ [
378
			'method' => 'PUT',
379
			'url' => [ $dstCont, $dstRel ],
380
			'headers' => [
381
				'x-copy-from' => '/' . rawurlencode( $srcCont ) .
382
					'/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
383
			] + $this->sanitizeHdrs( $params ), // extra headers merged into object
384
		] ];
385
386
		$method = __METHOD__;
387 View Code Duplication
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
388
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
389
			if ( $rcode === 201 ) {
0 ignored issues
show
Unused Code introduced by
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...
390
				// good
391
			} elseif ( $rcode === 404 ) {
392
				$status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
393
			} else {
394
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
395
			}
396
		};
397
398
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
399 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
400
			$status->value = $opHandle;
401
		} else { // actually write the object in Swift
402
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
403
		}
404
405
		return $status;
406
	}
407
408
	protected function doMoveInternal( array $params ) {
409
		$status = $this->newStatus();
410
411
		list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
412
		if ( $srcRel === null ) {
413
			$status->fatal( 'backend-fail-invalidpath', $params['src'] );
414
415
			return $status;
416
		}
417
418
		list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
419
		if ( $dstRel === null ) {
420
			$status->fatal( 'backend-fail-invalidpath', $params['dst'] );
421
422
			return $status;
423
		}
424
425
		$reqs = [
426
			[
427
				'method' => 'PUT',
428
				'url' => [ $dstCont, $dstRel ],
429
				'headers' => [
430
					'x-copy-from' => '/' . rawurlencode( $srcCont ) .
431
						'/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
432
				] + $this->sanitizeHdrs( $params ) // extra headers merged into object
433
			]
434
		];
435
		if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
436
			$reqs[] = [
437
				'method' => 'DELETE',
438
				'url' => [ $srcCont, $srcRel ],
439
				'headers' => []
440
			];
441
		}
442
443
		$method = __METHOD__;
444
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
445
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
446
			if ( $request['method'] === 'PUT' && $rcode === 201 ) {
0 ignored issues
show
Unused Code introduced by
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...
447
				// good
448
			} elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
449
				// good
450
			} elseif ( $rcode === 404 ) {
451
				$status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
452
			} else {
453
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
454
			}
455
		};
456
457
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
458 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
459
			$status->value = $opHandle;
460
		} else { // actually move the object in Swift
461
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
462
		}
463
464
		return $status;
465
	}
466
467
	protected function doDeleteInternal( array $params ) {
468
		$status = $this->newStatus();
469
470
		list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
471
		if ( $srcRel === null ) {
472
			$status->fatal( 'backend-fail-invalidpath', $params['src'] );
473
474
			return $status;
475
		}
476
477
		$reqs = [ [
478
			'method' => 'DELETE',
479
			'url' => [ $srcCont, $srcRel ],
480
			'headers' => []
481
		] ];
482
483
		$method = __METHOD__;
484 View Code Duplication
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
485
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
486
			if ( $rcode === 204 ) {
0 ignored issues
show
Unused Code introduced by
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...
487
				// good
488
			} elseif ( $rcode === 404 ) {
489
				if ( empty( $params['ignoreMissingSource'] ) ) {
490
					$status->fatal( 'backend-fail-delete', $params['src'] );
491
				}
492
			} else {
493
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
494
			}
495
		};
496
497
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
498 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
499
			$status->value = $opHandle;
500
		} else { // actually delete the object in Swift
501
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
502
		}
503
504
		return $status;
505
	}
506
507
	protected function doDescribeInternal( array $params ) {
508
		$status = $this->newStatus();
509
510
		list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
511
		if ( $srcRel === null ) {
512
			$status->fatal( 'backend-fail-invalidpath', $params['src'] );
513
514
			return $status;
515
		}
516
517
		// Fetch the old object headers/metadata...this should be in stat cache by now
518
		$stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
519
		if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
520
			$stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
521
		}
522
		if ( !$stat ) {
523
			$status->fatal( 'backend-fail-describe', $params['src'] );
524
525
			return $status;
526
		}
527
528
		// POST clears prior headers, so we need to merge the changes in to the old ones
529
		$metaHdrs = [];
530
		foreach ( $stat['xattr']['metadata'] as $name => $value ) {
531
			$metaHdrs["x-object-meta-$name"] = $value;
532
		}
533
		$customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
534
535
		$reqs = [ [
536
			'method' => 'POST',
537
			'url' => [ $srcCont, $srcRel ],
538
			'headers' => $metaHdrs + $customHdrs
539
		] ];
540
541
		$method = __METHOD__;
542 View Code Duplication
		$handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
543
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
544
			if ( $rcode === 202 ) {
0 ignored issues
show
Unused Code introduced by
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...
545
				// good
546
			} elseif ( $rcode === 404 ) {
547
				$status->fatal( 'backend-fail-describe', $params['src'] );
548
			} else {
549
				$this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
550
			}
551
		};
552
553
		$opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
554 View Code Duplication
		if ( !empty( $params['async'] ) ) { // deferred
555
			$status->value = $opHandle;
556
		} else { // actually change the object in Swift
557
			$status->merge( current( $this->doExecuteOpHandlesInternal( [ $opHandle ] ) ) );
0 ignored issues
show
Security Bug introduced by
It seems like current($this->doExecute...rnal(array($opHandle))) targeting current() can also be of type false; however, StatusValue::merge() does only seem to accept object<StatusValue>, did you maybe forget to handle an error condition?
Loading history...
558
		}
559
560
		return $status;
561
	}
562
563
	protected function doPrepareInternal( $fullCont, $dir, array $params ) {
564
		$status = $this->newStatus();
565
566
		// (a) Check if container already exists
567
		$stat = $this->getContainerStat( $fullCont );
568 View Code Duplication
		if ( is_array( $stat ) ) {
569
			return $status; // already there
570
		} elseif ( $stat === null ) {
571
			$status->fatal( 'backend-fail-internal', $this->name );
572
			$this->logger->error( __METHOD__ . ': cannot get container stat' );
573
574
			return $status;
575
		}
576
577
		// (b) Create container as needed with proper ACLs
578
		if ( $stat === false ) {
579
			$params['op'] = 'prepare';
580
			$status->merge( $this->createContainer( $fullCont, $params ) );
581
		}
582
583
		return $status;
584
	}
585
586 View Code Duplication
	protected function doSecureInternal( $fullCont, $dir, array $params ) {
587
		$status = $this->newStatus();
588
		if ( empty( $params['noAccess'] ) ) {
589
			return $status; // nothing to do
590
		}
591
592
		$stat = $this->getContainerStat( $fullCont );
593
		if ( is_array( $stat ) ) {
594
			// Make container private to end-users...
595
			$status->merge( $this->setContainerAccess(
596
				$fullCont,
597
				[ $this->swiftUser ], // read
598
				[ $this->swiftUser ] // write
599
			) );
600
		} elseif ( $stat === false ) {
601
			$status->fatal( 'backend-fail-usable', $params['dir'] );
602
		} else {
603
			$status->fatal( 'backend-fail-internal', $this->name );
604
			$this->logger->error( __METHOD__ . ': cannot get container stat' );
605
		}
606
607
		return $status;
608
	}
609
610 View Code Duplication
	protected function doPublishInternal( $fullCont, $dir, array $params ) {
611
		$status = $this->newStatus();
612
613
		$stat = $this->getContainerStat( $fullCont );
614
		if ( is_array( $stat ) ) {
615
			// Make container public to end-users...
616
			$status->merge( $this->setContainerAccess(
617
				$fullCont,
618
				[ $this->swiftUser, '.r:*' ], // read
619
				[ $this->swiftUser ] // write
620
			) );
621
		} elseif ( $stat === false ) {
622
			$status->fatal( 'backend-fail-usable', $params['dir'] );
623
		} else {
624
			$status->fatal( 'backend-fail-internal', $this->name );
625
			$this->logger->error( __METHOD__ . ': cannot get container stat' );
626
		}
627
628
		return $status;
629
	}
630
631
	protected function doCleanInternal( $fullCont, $dir, array $params ) {
632
		$status = $this->newStatus();
633
634
		// Only containers themselves can be removed, all else is virtual
635
		if ( $dir != '' ) {
636
			return $status; // nothing to do
637
		}
638
639
		// (a) Check the container
640
		$stat = $this->getContainerStat( $fullCont, true );
641 View Code Duplication
		if ( $stat === false ) {
642
			return $status; // ok, nothing to do
643
		} elseif ( !is_array( $stat ) ) {
644
			$status->fatal( 'backend-fail-internal', $this->name );
645
			$this->logger->error( __METHOD__ . ': cannot get container stat' );
646
647
			return $status;
648
		}
649
650
		// (b) Delete the container if empty
651
		if ( $stat['count'] == 0 ) {
652
			$params['op'] = 'clean';
653
			$status->merge( $this->deleteContainer( $fullCont, $params ) );
654
		}
655
656
		return $status;
657
	}
658
659
	protected function doGetFileStat( array $params ) {
660
		$params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
661
		unset( $params['src'] );
662
		$stats = $this->doGetFileStatMulti( $params );
663
664
		return reset( $stats );
665
	}
666
667
	/**
668
	 * Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
669
	 * Dates might also come in like "2013-05-11T07:37:27.678360" from Swift listings,
670
	 * missing the timezone suffix (though Ceph RGW does not appear to have this bug).
671
	 *
672
	 * @param string $ts
673
	 * @param int $format Output format (TS_* constant)
674
	 * @return string
675
	 * @throws FileBackendError
676
	 */
677
	protected function convertSwiftDate( $ts, $format = TS_MW ) {
678
		try {
679
			$timestamp = new MWTimestamp( $ts );
680
681
			return $timestamp->getTimestamp( $format );
682
		} catch ( Exception $e ) {
683
			throw new FileBackendError( $e->getMessage() );
684
		}
685
	}
686
687
	/**
688
	 * Fill in any missing object metadata and save it to Swift
689
	 *
690
	 * @param array $objHdrs Object response headers
691
	 * @param string $path Storage path to object
692
	 * @return array New headers
693
	 */
694
	protected function addMissingMetadata( array $objHdrs, $path ) {
695
		if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
696
			return $objHdrs; // nothing to do
697
		}
698
699
		/** @noinspection PhpUnusedLocalVariableInspection */
700
		$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
0 ignored issues
show
Unused Code introduced by
$ps is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
701
		$this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
702
703
		$objHdrs['x-object-meta-sha1base36'] = false;
704
705
		$auth = $this->getAuthentication();
706
		if ( !$auth ) {
707
			return $objHdrs; // failed
708
		}
709
710
		// Find prior custom HTTP headers
711
		$postHeaders = $this->getCustomHeaders( $objHdrs );
712
		// Find prior metadata headers
713
		$postHeaders += $this->getMetadataHeaders( $objHdrs );
714
715
		$status = $this->newStatus();
716
		/** @noinspection PhpUnusedLocalVariableInspection */
717
		$scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
0 ignored issues
show
Unused Code introduced by
$scopeLockS is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
718
		if ( $status->isOK() ) {
719
			$tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
720
			if ( $tmpFile ) {
721
				$hash = $tmpFile->getSha1Base36();
722
				if ( $hash !== false ) {
723
					$objHdrs['x-object-meta-sha1base36'] = $hash;
724
					// Merge new SHA1 header into the old ones
725
					$postHeaders['x-object-meta-sha1base36'] = $hash;
726
					list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
727
					list( $rcode ) = $this->http->run( [
728
						'method' => 'POST',
729
						'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
730
						'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
731
					] );
732
					if ( $rcode >= 200 && $rcode <= 299 ) {
733
						$this->deleteFileCache( $path );
734
735
						return $objHdrs; // success
736
					}
737
				}
738
			}
739
		}
740
741
		$this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
742
743
		return $objHdrs; // failed
744
	}
745
746
	protected function doGetFileContentsMulti( array $params ) {
747
		$contents = [];
748
749
		$auth = $this->getAuthentication();
750
751
		$ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
752
		// Blindly create tmp files and stream to them, catching any exception if the file does
753
		// not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
754
		$reqs = []; // (path => op)
755
756
		foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
757
			list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
758
			if ( $srcRel === null || !$auth ) {
759
				$contents[$path] = false;
760
				continue;
761
			}
762
			// Create a new temporary memory file...
763
			$handle = fopen( 'php://temp', 'wb' );
764 View Code Duplication
			if ( $handle ) {
765
				$reqs[$path] = [
766
					'method'  => 'GET',
767
					'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
768
					'headers' => $this->authTokenHeaders( $auth )
769
						+ $this->headersFromParams( $params ),
770
					'stream'  => $handle,
771
				];
772
			}
773
			$contents[$path] = false;
774
		}
775
776
		$opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
777
		$reqs = $this->http->runMulti( $reqs, $opts );
778
		foreach ( $reqs as $path => $op ) {
779
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
780
			if ( $rcode >= 200 && $rcode <= 299 ) {
781
				rewind( $op['stream'] ); // start from the beginning
782
				$contents[$path] = stream_get_contents( $op['stream'] );
783
			} elseif ( $rcode === 404 ) {
784
				$contents[$path] = false;
785
			} else {
786
				$this->onError( null, __METHOD__,
787
					[ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
788
			}
789
			fclose( $op['stream'] ); // close open handle
790
		}
791
792
		return $contents;
793
	}
794
795
	protected function doDirectoryExists( $fullCont, $dir, array $params ) {
796
		$prefix = ( $dir == '' ) ? null : "{$dir}/";
797
		$status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
798
		if ( $status->isOK() ) {
799
			return ( count( $status->value ) ) > 0;
800
		}
801
802
		return null; // error
803
	}
804
805
	/**
806
	 * @see FileBackendStore::getDirectoryListInternal()
807
	 * @param string $fullCont
808
	 * @param string $dir
809
	 * @param array $params
810
	 * @return SwiftFileBackendDirList
811
	 */
812
	public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
813
		return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
814
	}
815
816
	/**
817
	 * @see FileBackendStore::getFileListInternal()
818
	 * @param string $fullCont
819
	 * @param string $dir
820
	 * @param array $params
821
	 * @return SwiftFileBackendFileList
822
	 */
823
	public function getFileListInternal( $fullCont, $dir, array $params ) {
824
		return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
825
	}
826
827
	/**
828
	 * Do not call this function outside of SwiftFileBackendFileList
829
	 *
830
	 * @param string $fullCont Resolved container name
831
	 * @param string $dir Resolved storage directory with no trailing slash
832
	 * @param string|null $after Resolved container relative path to list items after
833
	 * @param int $limit Max number of items to list
834
	 * @param array $params Parameters for getDirectoryList()
835
	 * @return array List of container relative resolved paths of directories directly under $dir
836
	 * @throws FileBackendError
837
	 */
838
	public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
839
		$dirs = [];
840
		if ( $after === INF ) {
841
			return $dirs; // nothing more
842
		}
843
844
		$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
0 ignored issues
show
Unused Code introduced by
$ps is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
845
846
		$prefix = ( $dir == '' ) ? null : "{$dir}/";
847
		// Non-recursive: only list dirs right under $dir
848
		if ( !empty( $params['topOnly'] ) ) {
849
			$status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
850
			if ( !$status->isOK() ) {
851
				throw new FileBackendError( "Iterator page I/O error." );
852
			}
853
			$objects = $status->value;
854
			foreach ( $objects as $object ) { // files and directories
855
				if ( substr( $object, -1 ) === '/' ) {
856
					$dirs[] = $object; // directories end in '/'
857
				}
858
			}
859
		} else {
860
			// Recursive: list all dirs under $dir and its subdirs
861
			$getParentDir = function ( $path ) {
862
				return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
863
			};
864
865
			// Get directory from last item of prior page
866
			$lastDir = $getParentDir( $after ); // must be first page
867
			$status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
868
869
			if ( !$status->isOK() ) {
870
				throw new FileBackendError( "Iterator page I/O error." );
871
			}
872
873
			$objects = $status->value;
874
875
			foreach ( $objects as $object ) { // files
876
				$objectDir = $getParentDir( $object ); // directory of object
877
878
				if ( $objectDir !== false && $objectDir !== $dir ) {
879
					// Swift stores paths in UTF-8, using binary sorting.
880
					// See function "create_container_table" in common/db.py.
881
					// If a directory is not "greater" than the last one,
882
					// then it was already listed by the calling iterator.
883
					if ( strcmp( $objectDir, $lastDir ) > 0 ) {
884
						$pDir = $objectDir;
885
						do { // add dir and all its parent dirs
886
							$dirs[] = "{$pDir}/";
887
							$pDir = $getParentDir( $pDir );
888
						} while ( $pDir !== false // sanity
889
							&& strcmp( $pDir, $lastDir ) > 0 // not done already
890
							&& strlen( $pDir ) > strlen( $dir ) // within $dir
891
						);
892
					}
893
					$lastDir = $objectDir;
894
				}
895
			}
896
		}
897
		// Page on the unfiltered directory listing (what is returned may be filtered)
898
		if ( count( $objects ) < $limit ) {
899
			$after = INF; // avoid a second RTT
900
		} else {
901
			$after = end( $objects ); // update last item
902
		}
903
904
		return $dirs;
905
	}
906
907
	/**
908
	 * Do not call this function outside of SwiftFileBackendFileList
909
	 *
910
	 * @param string $fullCont Resolved container name
911
	 * @param string $dir Resolved storage directory with no trailing slash
912
	 * @param string|null $after Resolved container relative path of file to list items after
913
	 * @param int $limit Max number of items to list
914
	 * @param array $params Parameters for getDirectoryList()
915
	 * @return array List of resolved container relative paths of files under $dir
916
	 * @throws FileBackendError
917
	 */
918
	public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
919
		$files = []; // list of (path, stat array or null) entries
920
		if ( $after === INF ) {
921
			return $files; // nothing more
922
		}
923
924
		$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
0 ignored issues
show
Unused Code introduced by
$ps is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
925
926
		$prefix = ( $dir == '' ) ? null : "{$dir}/";
927
		// $objects will contain a list of unfiltered names or CF_Object items
928
		// Non-recursive: only list files right under $dir
929
		if ( !empty( $params['topOnly'] ) ) {
930 View Code Duplication
			if ( !empty( $params['adviseStat'] ) ) {
931
				$status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
932
			} else {
933
				$status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
934
			}
935 View Code Duplication
		} else {
936
			// Recursive: list all files under $dir and its subdirs
937
			if ( !empty( $params['adviseStat'] ) ) {
938
				$status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
939
			} else {
940
				$status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
941
			}
942
		}
943
944
		// Reformat this list into a list of (name, stat array or null) entries
945
		if ( !$status->isOK() ) {
946
			throw new FileBackendError( "Iterator page I/O error." );
947
		}
948
949
		$objects = $status->value;
950
		$files = $this->buildFileObjectListing( $params, $dir, $objects );
951
952
		// Page on the unfiltered object listing (what is returned may be filtered)
953
		if ( count( $objects ) < $limit ) {
954
			$after = INF; // avoid a second RTT
955
		} else {
956
			$after = end( $objects ); // update last item
957
			$after = is_object( $after ) ? $after->name : $after;
958
		}
959
960
		return $files;
961
	}
962
963
	/**
964
	 * Build a list of file objects, filtering out any directories
965
	 * and extracting any stat info if provided in $objects (for CF_Objects)
966
	 *
967
	 * @param array $params Parameters for getDirectoryList()
968
	 * @param string $dir Resolved container directory path
969
	 * @param array $objects List of CF_Object items or object names
970
	 * @return array List of (names,stat array or null) entries
971
	 */
972
	private function buildFileObjectListing( array $params, $dir, array $objects ) {
973
		$names = [];
974
		foreach ( $objects as $object ) {
975
			if ( is_object( $object ) ) {
976
				if ( isset( $object->subdir ) || !isset( $object->name ) ) {
977
					continue; // virtual directory entry; ignore
978
				}
979
				$stat = [
980
					// Convert various random Swift dates to TS_MW
981
					'mtime'  => $this->convertSwiftDate( $object->last_modified, TS_MW ),
982
					'size'   => (int)$object->bytes,
983
					'sha1'   => null,
984
					// Note: manifiest ETags are not an MD5 of the file
985
					'md5'    => ctype_xdigit( $object->hash ) ? $object->hash : null,
986
					'latest' => false // eventually consistent
987
				];
988
				$names[] = [ $object->name, $stat ];
989
			} elseif ( substr( $object, -1 ) !== '/' ) {
990
				// Omit directories, which end in '/' in listings
991
				$names[] = [ $object, null ];
992
			}
993
		}
994
995
		return $names;
996
	}
997
998
	/**
999
	 * Do not call this function outside of SwiftFileBackendFileList
1000
	 *
1001
	 * @param string $path Storage path
1002
	 * @param array $val Stat value
1003
	 */
1004
	public function loadListingStatInternal( $path, array $val ) {
1005
		$this->cheapCache->set( $path, 'stat', $val );
1006
	}
1007
1008 View Code Duplication
	protected function doGetFileXAttributes( array $params ) {
1009
		$stat = $this->getFileStat( $params );
1010
		if ( $stat ) {
1011
			if ( !isset( $stat['xattr'] ) ) {
1012
				// Stat entries filled by file listings don't include metadata/headers
1013
				$this->clearCache( [ $params['src'] ] );
1014
				$stat = $this->getFileStat( $params );
1015
			}
1016
1017
			return $stat['xattr'];
1018
		} else {
1019
			return false;
1020
		}
1021
	}
1022
1023 View Code Duplication
	protected function doGetFileSha1base36( array $params ) {
1024
		$stat = $this->getFileStat( $params );
1025
		if ( $stat ) {
1026
			if ( !isset( $stat['sha1'] ) ) {
1027
				// Stat entries filled by file listings don't include SHA1
1028
				$this->clearCache( [ $params['src'] ] );
1029
				$stat = $this->getFileStat( $params );
1030
			}
1031
1032
			return $stat['sha1'];
1033
		} else {
1034
			return false;
1035
		}
1036
	}
1037
1038
	protected function doStreamFile( array $params ) {
1039
		$status = $this->newStatus();
1040
1041
		$flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
1042
1043
		list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1044
		if ( $srcRel === null ) {
1045
			StreamFile::send404Message( $params['src'], $flags );
1046
			$status->fatal( 'backend-fail-invalidpath', $params['src'] );
1047
1048
			return $status;
1049
		}
1050
1051
		$auth = $this->getAuthentication();
1052
		if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
1053
			StreamFile::send404Message( $params['src'], $flags );
1054
			$status->fatal( 'backend-fail-stream', $params['src'] );
1055
1056
			return $status;
1057
		}
1058
1059
		// If "headers" is set, we only want to send them if the file is there.
1060
		// Do not bother checking if the file exists if headers are not set though.
1061
		if ( $params['headers'] && !$this->fileExists( $params ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fileExists($params) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1062
			StreamFile::send404Message( $params['src'], $flags );
1063
			$status->fatal( 'backend-fail-stream', $params['src'] );
1064
1065
			return $status;
1066
		}
1067
1068
		// Send the requested additional headers
1069
		foreach ( $params['headers'] as $header ) {
1070
			header( $header ); // aways send
1071
		}
1072
1073
		if ( empty( $params['allowOB'] ) ) {
1074
			// Cancel output buffering and gzipping if set
1075
			call_user_func( $this->obResetFunc );
1076
		}
1077
1078
		$handle = fopen( 'php://output', 'wb' );
1079
		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1080
			'method' => 'GET',
1081
			'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1082
			'headers' => $this->authTokenHeaders( $auth )
1083
				+ $this->headersFromParams( $params ) + $params['options'],
1084
			'stream' => $handle,
1085
			'flags'  => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
1086
		] );
1087
1088
		if ( $rcode >= 200 && $rcode <= 299 ) {
0 ignored issues
show
Unused Code introduced by
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...
1089
			// good
1090
		} elseif ( $rcode === 404 ) {
1091
			$status->fatal( 'backend-fail-stream', $params['src'] );
1092
			// Per bug 41113, nasty things can happen if bad cache entries get
1093
			// stuck in cache. It's also possible that this error can come up
1094
			// with simple race conditions. Clear out the stat cache to be safe.
1095
			$this->clearCache( [ $params['src'] ] );
1096
			$this->deleteFileCache( $params['src'] );
1097
		} else {
1098
			$this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1099
		}
1100
1101
		return $status;
1102
	}
1103
1104
	protected function doGetLocalCopyMulti( array $params ) {
1105
		/** @var TempFSFile[] $tmpFiles */
1106
		$tmpFiles = [];
1107
1108
		$auth = $this->getAuthentication();
1109
1110
		$ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
1111
		// Blindly create tmp files and stream to them, catching any exception if the file does
1112
		// not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
1113
		$reqs = []; // (path => op)
1114
1115
		foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
1116
			list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1117
			if ( $srcRel === null || !$auth ) {
1118
				$tmpFiles[$path] = null;
1119
				continue;
1120
			}
1121
			// Get source file extension
1122
			$ext = FileBackend::extensionFromPath( $path );
1123
			// Create a new temporary file...
1124
			$tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
1125
			if ( $tmpFile ) {
1126
				$handle = fopen( $tmpFile->getPath(), 'wb' );
1127 View Code Duplication
				if ( $handle ) {
1128
					$reqs[$path] = [
1129
						'method'  => 'GET',
1130
						'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
1131
						'headers' => $this->authTokenHeaders( $auth )
1132
							+ $this->headersFromParams( $params ),
1133
						'stream'  => $handle,
1134
					];
1135
				} else {
1136
					$tmpFile = null;
1137
				}
1138
			}
1139
			$tmpFiles[$path] = $tmpFile;
1140
		}
1141
1142
		$isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
1143
		$opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1144
		$reqs = $this->http->runMulti( $reqs, $opts );
1145
		foreach ( $reqs as $path => $op ) {
1146
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1147
			fclose( $op['stream'] ); // close open handle
1148
			if ( $rcode >= 200 && $rcode <= 299 ) {
1149
				$size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
1150
				// Double check that the disk is not full/broken
1151
				if ( $size != $rhdrs['content-length'] ) {
1152
					$tmpFiles[$path] = null;
1153
					$rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
1154
					$this->onError( null, __METHOD__,
1155
						[ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1156
				}
1157
				// Set the file stat process cache in passing
1158
				$stat = $this->getStatFromHeaders( $rhdrs );
1159
				$stat['latest'] = $isLatest;
1160
				$this->cheapCache->set( $path, 'stat', $stat );
1161
			} elseif ( $rcode === 404 ) {
1162
				$tmpFiles[$path] = false;
1163
			} else {
1164
				$tmpFiles[$path] = null;
1165
				$this->onError( null, __METHOD__,
1166
					[ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1167
			}
1168
		}
1169
1170
		return $tmpFiles;
1171
	}
1172
1173
	public function getFileHttpUrl( array $params ) {
1174
		if ( $this->swiftTempUrlKey != '' ||
1175
			( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
1176
		) {
1177
			list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1178
			if ( $srcRel === null ) {
1179
				return null; // invalid path
1180
			}
1181
1182
			$auth = $this->getAuthentication();
1183
			if ( !$auth ) {
1184
				return null;
1185
			}
1186
1187
			$ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
1188
			$expires = time() + $ttl;
1189
1190
			if ( $this->swiftTempUrlKey != '' ) {
1191
				$url = $this->storageUrl( $auth, $srcCont, $srcRel );
1192
				// Swift wants the signature based on the unencoded object name
1193
				$contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1194
				$signature = hash_hmac( 'sha1',
1195
					"GET\n{$expires}\n{$contPath}/{$srcRel}",
1196
					$this->swiftTempUrlKey
1197
				);
1198
1199
				return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
1200
			} else { // give S3 API URL for rgw
1201
				// Path for signature starts with the bucket
1202
				$spath = '/' . rawurlencode( $srcCont ) . '/' .
1203
					str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1204
				// Calculate the hash
1205
				$signature = base64_encode( hash_hmac(
1206
					'sha1',
1207
					"GET\n\n\n{$expires}\n{$spath}",
1208
					$this->rgwS3SecretKey,
1209
					true // raw
1210
				) );
1211
				// See https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
1212
				// Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
1213
				// Note: S3 API is the rgw default; remove the /swift/ URL bit.
1214
				return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
1215
					'?' .
1216
					http_build_query( [
1217
						'Signature' => $signature,
1218
						'Expires' => $expires,
1219
						'AWSAccessKeyId' => $this->rgwS3AccessKey
1220
					] );
1221
			}
1222
		}
1223
1224
		return null;
1225
	}
1226
1227
	protected function directoriesAreVirtual() {
1228
		return true;
1229
	}
1230
1231
	/**
1232
	 * Get headers to send to Swift when reading a file based
1233
	 * on a FileBackend params array, e.g. that of getLocalCopy().
1234
	 * $params is currently only checked for a 'latest' flag.
1235
	 *
1236
	 * @param array $params
1237
	 * @return array
1238
	 */
1239
	protected function headersFromParams( array $params ) {
1240
		$hdrs = [];
1241
		if ( !empty( $params['latest'] ) ) {
1242
			$hdrs['x-newest'] = 'true';
1243
		}
1244
1245
		return $hdrs;
1246
	}
1247
1248
	/**
1249
	 * @param FileBackendStoreOpHandle[] $fileOpHandles
1250
	 *
1251
	 * @return StatusValue[]
1252
	 */
1253
	protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1254
		/** @var $statuses StatusValue[] */
1255
		$statuses = [];
1256
1257
		$auth = $this->getAuthentication();
1258
		if ( !$auth ) {
1259
			foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1260
				$statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
1261
			}
1262
1263
			return $statuses;
1264
		}
1265
1266
		// Split the HTTP requests into stages that can be done concurrently
1267
		$httpReqsByStage = []; // map of (stage => index => HTTP request)
1268
		foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1269
			/** @var SwiftFileOpHandle $fileOpHandle */
1270
			$reqs = $fileOpHandle->httpOp;
1271
			// Convert the 'url' parameter to an actual URL using $auth
1272
			foreach ( $reqs as $stage => &$req ) {
1273
				list( $container, $relPath ) = $req['url'];
1274
				$req['url'] = $this->storageUrl( $auth, $container, $relPath );
1275
				$req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
1276
				$req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
1277
				$httpReqsByStage[$stage][$index] = $req;
1278
			}
1279
			$statuses[$index] = $this->newStatus();
1280
		}
1281
1282
		// Run all requests for the first stage, then the next, and so on
1283
		$reqCount = count( $httpReqsByStage );
1284
		for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1285
			$httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
1286
			foreach ( $httpReqs as $index => $httpReq ) {
1287
				// Run the callback for each request of this operation
1288
				$callback = $fileOpHandles[$index]->callback;
0 ignored issues
show
Bug introduced by
The property callback does not seem to exist in FileBackendStoreOpHandle.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1289
				call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
1290
				// On failure, abort all remaining requests for this operation
1291
				// (e.g. abort the DELETE request if the COPY request fails for a move)
1292
				if ( !$statuses[$index]->isOK() ) {
1293
					$stages = count( $fileOpHandles[$index]->httpOp );
0 ignored issues
show
Bug introduced by
The property httpOp does not seem to exist in FileBackendStoreOpHandle.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1294
					for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1295
						unset( $httpReqsByStage[$s][$index] );
1296
					}
1297
				}
1298
			}
1299
		}
1300
1301
		return $statuses;
1302
	}
1303
1304
	/**
1305
	 * Set read/write permissions for a Swift container.
1306
	 *
1307
	 * @see http://docs.openstack.org/developer/swift/misc.html#acls
1308
	 *
1309
	 * In general, we don't allow listings to end-users. It's not useful, isn't well-defined
1310
	 * (lists are truncated to 10000 item with no way to page), and is just a performance risk.
1311
	 *
1312
	 * @param string $container Resolved Swift container
1313
	 * @param array $readGrps List of the possible criteria for a request to have
1314
	 * access to read a container. Each item is one of the following formats:
1315
	 *   - account:user        : Grants access if the request is by the given user
1316
	 *   - ".r:<regex>"        : Grants access if the request is from a referrer host that
1317
	 *                           matches the expression and the request is not for a listing.
1318
	 *                           Setting this to '*' effectively makes a container public.
1319
	 *   -".rlistings:<regex>" : Grants access if the request is from a referrer host that
1320
	 *                           matches the expression and the request is for a listing.
1321
	 * @param array $writeGrps A list of the possible criteria for a request to have
1322
	 * access to write to a container. Each item is of the following format:
1323
	 *   - account:user       : Grants access if the request is by the given user
1324
	 * @return StatusValue
1325
	 */
1326
	protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
1327
		$status = $this->newStatus();
1328
		$auth = $this->getAuthentication();
1329
1330
		if ( !$auth ) {
1331
			$status->fatal( 'backend-fail-connect', $this->name );
1332
1333
			return $status;
1334
		}
1335
1336
		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rdesc is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rerr is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1337
			'method' => 'POST',
1338
			'url' => $this->storageUrl( $auth, $container ),
1339
			'headers' => $this->authTokenHeaders( $auth ) + [
1340
				'x-container-read' => implode( ',', $readGrps ),
1341
				'x-container-write' => implode( ',', $writeGrps )
1342
			]
1343
		] );
1344
1345
		if ( $rcode != 204 && $rcode !== 202 ) {
1346
			$status->fatal( 'backend-fail-internal', $this->name );
1347
			$this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
1348
		}
1349
1350
		return $status;
1351
	}
1352
1353
	/**
1354
	 * Get a Swift container stat array, possibly from process cache.
1355
	 * Use $reCache if the file count or byte count is needed.
1356
	 *
1357
	 * @param string $container Container name
1358
	 * @param bool $bypassCache Bypass all caches and load from Swift
1359
	 * @return array|bool|null False on 404, null on failure
1360
	 */
1361
	protected function getContainerStat( $container, $bypassCache = false ) {
1362
		$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
0 ignored issues
show
Unused Code introduced by
$ps is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1363
1364
		if ( $bypassCache ) { // purge cache
1365
			$this->containerStatCache->clear( $container );
1366
		} elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1367
			$this->primeContainerCache( [ $container ] ); // check persistent cache
1368
		}
1369
		if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1370
			$auth = $this->getAuthentication();
1371
			if ( !$auth ) {
1372
				return null;
1373
			}
1374
1375
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1376
				'method' => 'HEAD',
1377
				'url' => $this->storageUrl( $auth, $container ),
1378
				'headers' => $this->authTokenHeaders( $auth )
1379
			] );
1380
1381
			if ( $rcode === 204 ) {
1382
				$stat = [
1383
					'count' => $rhdrs['x-container-object-count'],
1384
					'bytes' => $rhdrs['x-container-bytes-used']
1385
				];
1386
				if ( $bypassCache ) {
1387
					return $stat;
1388
				} else {
1389
					$this->containerStatCache->set( $container, 'stat', $stat ); // cache it
1390
					$this->setContainerCache( $container, $stat ); // update persistent cache
1391
				}
1392
			} elseif ( $rcode === 404 ) {
1393
				return false;
1394
			} else {
1395
				$this->onError( null, __METHOD__,
1396
					[ 'cont' => $container ], $rerr, $rcode, $rdesc );
1397
1398
				return null;
1399
			}
1400
		}
1401
1402
		return $this->containerStatCache->get( $container, 'stat' );
1403
	}
1404
1405
	/**
1406
	 * Create a Swift container
1407
	 *
1408
	 * @param string $container Container name
1409
	 * @param array $params
1410
	 * @return StatusValue
1411
	 */
1412
	protected function createContainer( $container, array $params ) {
1413
		$status = $this->newStatus();
1414
1415
		$auth = $this->getAuthentication();
1416
		if ( !$auth ) {
1417
			$status->fatal( 'backend-fail-connect', $this->name );
1418
1419
			return $status;
1420
		}
1421
1422
		// @see SwiftFileBackend::setContainerAccess()
1423
		if ( empty( $params['noAccess'] ) ) {
1424
			$readGrps = [ '.r:*', $this->swiftUser ]; // public
1425
		} else {
1426
			$readGrps = [ $this->swiftUser ]; // private
1427
		}
1428
		$writeGrps = [ $this->swiftUser ]; // sanity
1429
1430
		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1431
			'method' => 'PUT',
1432
			'url' => $this->storageUrl( $auth, $container ),
1433
			'headers' => $this->authTokenHeaders( $auth ) + [
1434
				'x-container-read' => implode( ',', $readGrps ),
1435
				'x-container-write' => implode( ',', $writeGrps )
1436
			]
1437
		] );
1438
1439
		if ( $rcode === 201 ) { // new
0 ignored issues
show
Unused Code introduced by
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...
1440
			// good
1441
		} elseif ( $rcode === 202 ) { // already there
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
1442
			// this shouldn't really happen, but is OK
1443
		} else {
1444
			$this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1445
		}
1446
1447
		return $status;
1448
	}
1449
1450
	/**
1451
	 * Delete a Swift container
1452
	 *
1453
	 * @param string $container Container name
1454
	 * @param array $params
1455
	 * @return StatusValue
1456
	 */
1457
	protected function deleteContainer( $container, array $params ) {
1458
		$status = $this->newStatus();
1459
1460
		$auth = $this->getAuthentication();
1461
		if ( !$auth ) {
1462
			$status->fatal( 'backend-fail-connect', $this->name );
1463
1464
			return $status;
1465
		}
1466
1467
		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1468
			'method' => 'DELETE',
1469
			'url' => $this->storageUrl( $auth, $container ),
1470
			'headers' => $this->authTokenHeaders( $auth )
1471
		] );
1472
1473
		if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
1474
			$this->containerStatCache->clear( $container ); // purge
1475
		} elseif ( $rcode === 404 ) { // not there
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif 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 elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
1476
			// this shouldn't really happen, but is OK
1477
		} elseif ( $rcode === 409 ) { // not empty
1478
			$this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
1479
		} else {
1480
			$this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1481
		}
1482
1483
		return $status;
1484
	}
1485
1486
	/**
1487
	 * Get a list of objects under a container.
1488
	 * Either just the names or a list of stdClass objects with details can be returned.
1489
	 *
1490
	 * @param string $fullCont
1491
	 * @param string $type ('info' for a list of object detail maps, 'names' for names only)
1492
	 * @param int $limit
1493
	 * @param string|null $after
1494
	 * @param string|null $prefix
1495
	 * @param string|null $delim
1496
	 * @return StatusValue With the list as value
1497
	 */
1498
	private function objectListing(
1499
		$fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
1500
	) {
1501
		$status = $this->newStatus();
1502
1503
		$auth = $this->getAuthentication();
1504
		if ( !$auth ) {
1505
			$status->fatal( 'backend-fail-connect', $this->name );
1506
1507
			return $status;
1508
		}
1509
1510
		$query = [ 'limit' => $limit ];
1511
		if ( $type === 'info' ) {
1512
			$query['format'] = 'json';
1513
		}
1514
		if ( $after !== null ) {
1515
			$query['marker'] = $after;
1516
		}
1517
		if ( $prefix !== null ) {
1518
			$query['prefix'] = $prefix;
1519
		}
1520
		if ( $delim !== null ) {
1521
			$query['delimiter'] = $delim;
1522
		}
1523
1524
		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rhdrs is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1525
			'method' => 'GET',
1526
			'url' => $this->storageUrl( $auth, $fullCont ),
1527
			'query' => $query,
1528
			'headers' => $this->authTokenHeaders( $auth )
1529
		] );
1530
1531
		$params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
1532
		if ( $rcode === 200 ) { // good
1533
			if ( $type === 'info' ) {
1534
				$status->value = FormatJson::decode( trim( $rbody ) );
1535
			} else {
1536
				$status->value = explode( "\n", trim( $rbody ) );
1537
			}
1538
		} elseif ( $rcode === 204 ) {
1539
			$status->value = []; // empty container
1540
		} elseif ( $rcode === 404 ) {
1541
			$status->value = []; // no container
1542
		} else {
1543
			$this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1544
		}
1545
1546
		return $status;
1547
	}
1548
1549
	protected function doPrimeContainerCache( array $containerInfo ) {
1550
		foreach ( $containerInfo as $container => $info ) {
1551
			$this->containerStatCache->set( $container, 'stat', $info );
1552
		}
1553
	}
1554
1555
	protected function doGetFileStatMulti( array $params ) {
1556
		$stats = [];
1557
1558
		$auth = $this->getAuthentication();
1559
1560
		$reqs = [];
1561
		foreach ( $params['srcs'] as $path ) {
1562
			list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1563
			if ( $srcRel === null ) {
1564
				$stats[$path] = false;
1565
				continue; // invalid storage path
1566
			} elseif ( !$auth ) {
1567
				$stats[$path] = null;
1568
				continue;
1569
			}
1570
1571
			// (a) Check the container
1572
			$cstat = $this->getContainerStat( $srcCont );
1573
			if ( $cstat === false ) {
1574
				$stats[$path] = false;
1575
				continue; // ok, nothing to do
1576
			} elseif ( !is_array( $cstat ) ) {
1577
				$stats[$path] = null;
1578
				continue;
1579
			}
1580
1581
			$reqs[$path] = [
1582
				'method'  => 'HEAD',
1583
				'url'     => $this->storageUrl( $auth, $srcCont, $srcRel ),
1584
				'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
1585
			];
1586
		}
1587
1588
		$opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1589
		$reqs = $this->http->runMulti( $reqs, $opts );
1590
1591
		foreach ( $params['srcs'] as $path ) {
1592
			if ( array_key_exists( $path, $stats ) ) {
1593
				continue; // some sort of failure above
1594
			}
1595
			// (b) Check the file
1596
			list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
0 ignored issues
show
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1597
			if ( $rcode === 200 || $rcode === 204 ) {
1598
				// Update the object if it is missing some headers
1599
				$rhdrs = $this->addMissingMetadata( $rhdrs, $path );
1600
				// Load the stat array from the headers
1601
				$stat = $this->getStatFromHeaders( $rhdrs );
1602
				if ( $this->isRGW ) {
1603
					$stat['latest'] = true; // strong consistency
1604
				}
1605
			} elseif ( $rcode === 404 ) {
1606
				$stat = false;
1607
			} else {
1608
				$stat = null;
1609
				$this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
1610
			}
1611
			$stats[$path] = $stat;
1612
		}
1613
1614
		return $stats;
1615
	}
1616
1617
	/**
1618
	 * @param array $rhdrs
1619
	 * @return array
1620
	 */
1621
	protected function getStatFromHeaders( array $rhdrs ) {
1622
		// Fetch all of the custom metadata headers
1623
		$metadata = $this->getMetadata( $rhdrs );
1624
		// Fetch all of the custom raw HTTP headers
1625
		$headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
1626
1627
		return [
1628
			// Convert various random Swift dates to TS_MW
1629
			'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
1630
			// Empty objects actually return no content-length header in Ceph
1631
			'size'  => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
1632
			'sha1'  => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
1633
			// Note: manifiest ETags are not an MD5 of the file
1634
			'md5'   => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
1635
			'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
1636
		];
1637
	}
1638
1639
	/**
1640
	 * @return array|null Credential map
1641
	 */
1642
	protected function getAuthentication() {
1643
		if ( $this->authErrorTimestamp !== null ) {
1644
			if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
1645
				return null; // failed last attempt; don't bother
1646
			} else { // actually retry this time
1647
				$this->authErrorTimestamp = null;
1648
			}
1649
		}
1650
		// Session keys expire after a while, so we renew them periodically
1651
		$reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
1652
		// Authenticate with proxy and get a session key...
1653
		if ( !$this->authCreds || $reAuth ) {
1654
			$this->authSessionTimestamp = 0;
1655
			$cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1656
			$creds = $this->srvCache->get( $cacheKey ); // credentials
1657
			// Try to use the credential cache
1658
			if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
1659
				$this->authCreds = $creds;
0 ignored issues
show
Documentation Bug introduced by
It seems like $creds of type * is incompatible with the declared type array of property $authCreds.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1660
				// Skew the timestamp for worst case to avoid using stale credentials
1661
				$this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
0 ignored issues
show
Documentation Bug introduced by
The property $authSessionTimestamp was declared of type integer, but time() - ceil($this->authTTL / 2) is of type double. 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...
1662
			} else { // cache miss
1663
				list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
0 ignored issues
show
Unused Code introduced by
The assignment to $rdesc is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rbody is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $rerr is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
1664
					'method' => 'GET',
1665
					'url' => "{$this->swiftAuthUrl}/v1.0",
1666
					'headers' => [
1667
						'x-auth-user' => $this->swiftUser,
1668
						'x-auth-key' => $this->swiftKey
1669
					]
1670
				] );
1671
1672
				if ( $rcode >= 200 && $rcode <= 299 ) { // OK
1673
					$this->authCreds = [
1674
						'auth_token' => $rhdrs['x-auth-token'],
1675
						'storage_url' => $rhdrs['x-storage-url']
1676
					];
1677
					$this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
1678
					$this->authSessionTimestamp = time();
1679
				} elseif ( $rcode === 401 ) {
1680
					$this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
1681
					$this->authErrorTimestamp = time();
1682
1683
					return null;
1684
				} else {
1685
					$this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
1686
					$this->authErrorTimestamp = time();
1687
1688
					return null;
1689
				}
1690
			}
1691
			// Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
1692
			if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
1693
				$this->isRGW = true; // take advantage of strong consistency in Ceph
1694
			}
1695
		}
1696
1697
		return $this->authCreds;
1698
	}
1699
1700
	/**
1701
	 * @param array $creds From getAuthentication()
1702
	 * @param string $container
1703
	 * @param string $object
1704
	 * @return array
1705
	 */
1706
	protected function storageUrl( array $creds, $container = null, $object = null ) {
1707
		$parts = [ $creds['storage_url'] ];
1708
		if ( strlen( $container ) ) {
1709
			$parts[] = rawurlencode( $container );
1710
		}
1711
		if ( strlen( $object ) ) {
1712
			$parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
1713
		}
1714
1715
		return implode( '/', $parts );
1716
	}
1717
1718
	/**
1719
	 * @param array $creds From getAuthentication()
1720
	 * @return array
1721
	 */
1722
	protected function authTokenHeaders( array $creds ) {
1723
		return [ 'x-auth-token' => $creds['auth_token'] ];
1724
	}
1725
1726
	/**
1727
	 * Get the cache key for a container
1728
	 *
1729
	 * @param string $username
1730
	 * @return string
1731
	 */
1732
	private function getCredsCacheKey( $username ) {
1733
		return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
1734
	}
1735
1736
	/**
1737
	 * Log an unexpected exception for this backend.
1738
	 * This also sets the StatusValue object to have a fatal error.
1739
	 *
1740
	 * @param StatusValue|null $status
1741
	 * @param string $func
1742
	 * @param array $params
1743
	 * @param string $err Error string
1744
	 * @param int $code HTTP status
1745
	 * @param string $desc HTTP StatusValue description
1746
	 */
1747
	public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
1748
		if ( $status instanceof StatusValue ) {
1749
			$status->fatal( 'backend-fail-internal', $this->name );
1750
		}
1751
		if ( $code == 401 ) { // possibly a stale token
1752
			$this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
1753
		}
1754
		$this->logger->error(
1755
			"HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
1756
			( $err ? ": $err" : "" )
1757
		);
1758
	}
1759
}
1760
1761
/**
1762
 * @see FileBackendStoreOpHandle
1763
 */
1764
class SwiftFileOpHandle extends FileBackendStoreOpHandle {
1765
	/** @var array List of Requests for MultiHttpClient */
1766
	public $httpOp;
1767
	/** @var Closure */
1768
	public $callback;
1769
1770
	/**
1771
	 * @param SwiftFileBackend $backend
1772
	 * @param Closure $callback Function that takes (HTTP request array, status)
1773
	 * @param array $httpOp MultiHttpClient op
1774
	 */
1775
	public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
1776
		$this->backend = $backend;
1777
		$this->callback = $callback;
1778
		$this->httpOp = $httpOp;
1779
	}
1780
}
1781
1782
/**
1783
 * SwiftFileBackend helper class to page through listings.
1784
 * Swift also has a listing limit of 10,000 objects for sanity.
1785
 * Do not use this class from places outside SwiftFileBackend.
1786
 *
1787
 * @ingroup FileBackend
1788
 */
1789
abstract class SwiftFileBackendList implements Iterator {
1790
	/** @var array List of path or (path,stat array) entries */
1791
	protected $bufferIter = [];
1792
1793
	/** @var string List items *after* this path */
1794
	protected $bufferAfter = null;
1795
1796
	/** @var int */
1797
	protected $pos = 0;
1798
1799
	/** @var array */
1800
	protected $params = [];
1801
1802
	/** @var SwiftFileBackend */
1803
	protected $backend;
1804
1805
	/** @var string Container name */
1806
	protected $container;
1807
1808
	/** @var string Storage directory */
1809
	protected $dir;
1810
1811
	/** @var int */
1812
	protected $suffixStart;
1813
1814
	const PAGE_SIZE = 9000; // file listing buffer size
1815
1816
	/**
1817
	 * @param SwiftFileBackend $backend
1818
	 * @param string $fullCont Resolved container name
1819
	 * @param string $dir Resolved directory relative to container
1820
	 * @param array $params
1821
	 */
1822
	public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
1823
		$this->backend = $backend;
1824
		$this->container = $fullCont;
1825
		$this->dir = $dir;
1826
		if ( substr( $this->dir, -1 ) === '/' ) {
1827
			$this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
1828
		}
1829
		if ( $this->dir == '' ) { // whole container
1830
			$this->suffixStart = 0;
1831
		} else { // dir within container
1832
			$this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
1833
		}
1834
		$this->params = $params;
1835
	}
1836
1837
	/**
1838
	 * @see Iterator::key()
1839
	 * @return int
1840
	 */
1841
	public function key() {
1842
		return $this->pos;
1843
	}
1844
1845
	/**
1846
	 * @see Iterator::next()
1847
	 */
1848
	public function next() {
1849
		// Advance to the next file in the page
1850
		next( $this->bufferIter );
1851
		++$this->pos;
1852
		// Check if there are no files left in this page and
1853
		// advance to the next page if this page was not empty.
1854
		if ( !$this->valid() && count( $this->bufferIter ) ) {
1855
			$this->bufferIter = $this->pageFromList(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->pageFromList($thi...GE_SIZE, $this->params) can also be of type object<Traversable>. However, the property $bufferIter is declared as type array. 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...
1856
				$this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1857
			); // updates $this->bufferAfter
1858
		}
1859
	}
1860
1861
	/**
1862
	 * @see Iterator::rewind()
1863
	 */
1864
	public function rewind() {
1865
		$this->pos = 0;
1866
		$this->bufferAfter = null;
1867
		$this->bufferIter = $this->pageFromList(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->pageFromList($thi...GE_SIZE, $this->params) can also be of type object<Traversable>. However, the property $bufferIter is declared as type array. 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...
1868
			$this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1869
		); // updates $this->bufferAfter
1870
	}
1871
1872
	/**
1873
	 * @see Iterator::valid()
1874
	 * @return bool
1875
	 */
1876
	public function valid() {
1877
		if ( $this->bufferIter === null ) {
1878
			return false; // some failure?
1879
		} else {
1880
			return ( current( $this->bufferIter ) !== false ); // no paths can have this value
1881
		}
1882
	}
1883
1884
	/**
1885
	 * Get the given list portion (page)
1886
	 *
1887
	 * @param string $container Resolved container name
1888
	 * @param string $dir Resolved path relative to container
1889
	 * @param string $after
1890
	 * @param int $limit
1891
	 * @param array $params
1892
	 * @return Traversable|array
1893
	 */
1894
	abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
1895
}
1896
1897
/**
1898
 * Iterator for listing directories
1899
 */
1900
class SwiftFileBackendDirList extends SwiftFileBackendList {
1901
	/**
1902
	 * @see Iterator::current()
1903
	 * @return string|bool String (relative path) or false
1904
	 */
1905
	public function current() {
1906
		return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
1907
	}
1908
1909
	protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1910
		return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
1911
	}
1912
}
1913
1914
/**
1915
 * Iterator for listing regular files
1916
 */
1917
class SwiftFileBackendFileList extends SwiftFileBackendList {
1918
	/**
1919
	 * @see Iterator::current()
1920
	 * @return string|bool String (relative path) or false
1921
	 */
1922
	public function current() {
1923
		list( $path, $stat ) = current( $this->bufferIter );
1924
		$relPath = substr( $path, $this->suffixStart );
1925
		if ( is_array( $stat ) ) {
1926
			$storageDir = rtrim( $this->params['dir'], '/' );
1927
			$this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
1928
		}
1929
1930
		return $relPath;
1931
	}
1932
1933
	protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1934
		return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
1935
	}
1936
}
1937