Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
18.43% |
113 / 613 |
|
10.71% |
9 / 84 |
CRAP | |
0.00% |
0 / 1 |
FileRepo | |
18.43% |
113 / 613 |
|
10.71% |
9 / 84 |
31497.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
14 | |||
getBackend | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReadOnlyReason | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initZones | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
isVirtualUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getVirtualUrl | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getZoneUrl | |
40.00% |
6 / 15 |
|
0.00% |
0 / 1 |
37.14 | |||
backendSupportsUnicodePaths | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
resolveVirtualUrl | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
getZoneLocation | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getZonePath | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
newFile | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
6.00 | |||
findFile | |
37.50% |
15 / 40 |
|
0.00% |
0 / 1 |
128.67 | |||
findFiles | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 | |||
findFileFromKey | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
182 | |||
findBySha1 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findBySha1s | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
findFilesByPrefix | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getThumbScriptUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getThumbProxyUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getThumbProxySecret | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canTransformVia404 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canTransformLocally | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNameFromTitle | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
3.71 | |||
getRootDirectory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHashPath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTempHashPath | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getHashPathForLevel | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getHashLevels | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeUrl | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getDescriptionUrl | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getDescriptionRenderUrl | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getDescriptionStylesheetUrl | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
store | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
storeBatch | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
110 | |||
cleanupBatch | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
quickImport | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
quickImportBatch | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
72 | |||
quickPurge | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
quickCleanDir | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
quickPurgeBatch | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
storeTemp | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
freeTemp | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
concatenate | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
publish | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
publishBatch | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
182 | |||
initDirectory | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
cleanDir | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
fileExists | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
fileExistsBatch | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
delete | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
deleteBatch | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
30 | |||
cleanupDeletedBatch | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDeletedHashPath | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
resolveToStoragePathIfVirtual | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getLocalCopy | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getLocalReference | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFileProps | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getFileTimestamp | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFileSize | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getFileSha1 | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
streamFileWithStatus | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
enumFiles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enumFilesInStorage | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
validateFilename | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getErrorCleanupFunction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
paranoidClean | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
passThrough | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newFatal | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
newGood | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkRedirect | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
invalidateImageRedirect | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDisplayName | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
nameForThumb | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
isLocal | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSharedCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLocalCacheKey | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getTempRepo | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
getUploadStash | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
assertWritableRepo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getInfo | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
4 | |||
hasSha1Storage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
supportsSha1URLs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * @defgroup FileRepo File Repository |
4 | * |
5 | * @brief This module handles how MediaWiki interacts with filesystems. |
6 | * |
7 | * @details |
8 | */ |
9 | |
10 | use MediaWiki\Context\RequestContext; |
11 | use MediaWiki\Linker\LinkTarget; |
12 | use MediaWiki\MainConfigNames; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Page\PageIdentity; |
15 | use MediaWiki\Permissions\Authority; |
16 | use MediaWiki\Status\Status; |
17 | use MediaWiki\Title\Title; |
18 | use MediaWiki\User\UserIdentity; |
19 | use MediaWiki\Utils\MWTimestamp; |
20 | use Wikimedia\AtEase\AtEase; |
21 | |
22 | /** |
23 | * Base code for file repositories. |
24 | * |
25 | * This program is free software; you can redistribute it and/or modify |
26 | * it under the terms of the GNU General Public License as published by |
27 | * the Free Software Foundation; either version 2 of the License, or |
28 | * (at your option) any later version. |
29 | * |
30 | * This program is distributed in the hope that it will be useful, |
31 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
32 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
33 | * GNU General Public License for more details. |
34 | * |
35 | * You should have received a copy of the GNU General Public License along |
36 | * with this program; if not, write to the Free Software Foundation, Inc., |
37 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
38 | * http://www.gnu.org/copyleft/gpl.html |
39 | * |
40 | * @file |
41 | * @ingroup FileRepo |
42 | */ |
43 | |
44 | /** |
45 | * Base class for file repositories |
46 | * |
47 | * See [the architecture doc](@ref filerepoarch) for more information. |
48 | * |
49 | * @ingroup FileRepo |
50 | */ |
51 | class FileRepo { |
52 | public const DELETE_SOURCE = 1; |
53 | public const OVERWRITE = 2; |
54 | public const OVERWRITE_SAME = 4; |
55 | public const SKIP_LOCKING = 8; |
56 | |
57 | public const NAME_AND_TIME_ONLY = 1; |
58 | |
59 | /** @var bool Whether to fetch commons image description pages and display |
60 | * them on the local wiki |
61 | */ |
62 | public $fetchDescription; |
63 | |
64 | /** @var int */ |
65 | public $descriptionCacheExpiry; |
66 | |
67 | /** @var bool */ |
68 | protected $hasSha1Storage = false; |
69 | |
70 | /** @var bool */ |
71 | protected $supportsSha1URLs = false; |
72 | |
73 | /** @var FileBackend */ |
74 | protected $backend; |
75 | |
76 | /** @var array Map of zones to config */ |
77 | protected $zones = []; |
78 | |
79 | /** @var string URL of thumb.php */ |
80 | protected $thumbScriptUrl; |
81 | |
82 | /** @var bool Whether to skip media file transformation on parse and rely |
83 | * on a 404 handler instead. |
84 | */ |
85 | protected $transformVia404; |
86 | |
87 | /** @var string URL of image description pages, e.g. |
88 | * https://en.wikipedia.org/wiki/File: |
89 | */ |
90 | protected $descBaseUrl; |
91 | |
92 | /** @var string URL of the MediaWiki installation, equivalent to |
93 | * $wgScriptPath, e.g. https://en.wikipedia.org/w |
94 | */ |
95 | protected $scriptDirUrl; |
96 | |
97 | /** @var string Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 */ |
98 | protected $articleUrl; |
99 | |
100 | /** @var bool Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], |
101 | * determines whether filenames implicitly start with a capital letter. |
102 | * The current implementation may give incorrect description page links |
103 | * when the local $wgCapitalLinks and initialCapital are mismatched. |
104 | */ |
105 | protected $initialCapital; |
106 | |
107 | /** @var string May be 'paranoid' to remove all parameters from error |
108 | * messages, 'none' to leave the paths in unchanged, or 'simple' to |
109 | * replace paths with placeholders. Default for LocalRepo is |
110 | * 'simple'. |
111 | */ |
112 | protected $pathDisclosureProtection = 'simple'; |
113 | |
114 | /** @var string|false Public zone URL. */ |
115 | protected $url; |
116 | |
117 | /** @var string|false The base thumbnail URL. Defaults to "<url>/thumb". */ |
118 | protected $thumbUrl; |
119 | |
120 | /** @var int The number of directory levels for hash-based division of files */ |
121 | protected $hashLevels; |
122 | |
123 | /** @var int The number of directory levels for hash-based division of deleted files */ |
124 | protected $deletedHashLevels; |
125 | |
126 | /** @var int File names over this size will use the short form of thumbnail |
127 | * names. Short thumbnail names only have the width, parameters, and the |
128 | * extension. |
129 | */ |
130 | protected $abbrvThreshold; |
131 | |
132 | /** @var null|string The URL to a favicon (optional, may be a server-local path URL). */ |
133 | protected $favicon = null; |
134 | |
135 | /** @var bool Whether all zones should be private (e.g. private wiki repo) */ |
136 | protected $isPrivate; |
137 | |
138 | /** @var callable Override these in the base class */ |
139 | protected $fileFactory = [ UnregisteredLocalFile::class, 'newFromTitle' ]; |
140 | /** @var callable|false Override these in the base class */ |
141 | protected $oldFileFactory = false; |
142 | /** @var callable|false Override these in the base class */ |
143 | protected $fileFactoryKey = false; |
144 | /** @var callable|false Override these in the base class */ |
145 | protected $oldFileFactoryKey = false; |
146 | |
147 | /** @var string URL of where to proxy thumb.php requests to. |
148 | * Example: http://127.0.0.1:8888/wiki/dev/thumb/ |
149 | */ |
150 | protected $thumbProxyUrl; |
151 | /** @var string Secret key to pass as an X-Swift-Secret header to the proxied thumb service */ |
152 | protected $thumbProxySecret; |
153 | |
154 | /** @var bool Disable local image scaling */ |
155 | protected $disableLocalTransform = false; |
156 | |
157 | /** @var WANObjectCache */ |
158 | protected $wanCache; |
159 | |
160 | /** |
161 | * @var string |
162 | * @note Use $this->getName(). Public for back-compat only |
163 | * @todo make protected |
164 | */ |
165 | public $name; |
166 | |
167 | /** |
168 | * @see Documentation of info options at $wgLocalFileRepo |
169 | * @param array|null $info |
170 | * @phan-assert array $info |
171 | */ |
172 | public function __construct( array $info = null ) { |
173 | // Verify required settings presence |
174 | if ( |
175 | $info === null |
176 | || !array_key_exists( 'name', $info ) |
177 | || !array_key_exists( 'backend', $info ) |
178 | ) { |
179 | throw new InvalidArgumentException( __CLASS__ . |
180 | " requires an array of options having both 'name' and 'backend' keys.\n" ); |
181 | } |
182 | |
183 | // Required settings |
184 | $this->name = $info['name']; |
185 | if ( $info['backend'] instanceof FileBackend ) { |
186 | $this->backend = $info['backend']; // useful for testing |
187 | } else { |
188 | $this->backend = |
189 | MediaWikiServices::getInstance()->getFileBackendGroup()->get( $info['backend'] ); |
190 | } |
191 | |
192 | // Optional settings that can have no value |
193 | $optionalSettings = [ |
194 | 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', |
195 | 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', |
196 | 'favicon', 'thumbProxyUrl', 'thumbProxySecret', 'disableLocalTransform' |
197 | ]; |
198 | foreach ( $optionalSettings as $var ) { |
199 | if ( isset( $info[$var] ) ) { |
200 | $this->$var = $info[$var]; |
201 | } |
202 | } |
203 | |
204 | // Optional settings that have a default |
205 | $localCapitalLinks = |
206 | MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE ); |
207 | $this->initialCapital = $info['initialCapital'] ?? $localCapitalLinks; |
208 | if ( $localCapitalLinks && !$this->initialCapital ) { |
209 | // If the local wiki's file namespace requires an initial capital, but a foreign file |
210 | // repo doesn't, complications will result. Linker code will want to auto-capitalize the |
211 | // first letter of links to files, but those links might actually point to files on |
212 | // foreign wikis with initial-lowercase names. This combination is not likely to be |
213 | // used by anyone anyway, so we just outlaw it to save ourselves the bugs. If you want |
214 | // to include a foreign file repo with initialCapital false, set your local file |
215 | // namespace to not be capitalized either. |
216 | throw new InvalidArgumentException( |
217 | 'File repos with initial capital false are not allowed on wikis where the File ' . |
218 | 'namespace has initial capital true' ); |
219 | } |
220 | |
221 | $this->url = $info['url'] ?? false; // a subclass may set the URL (e.g. ForeignAPIRepo) |
222 | $defaultThumbUrl = $this->url ? $this->url . '/thumb' : false; |
223 | $this->thumbUrl = $info['thumbUrl'] ?? $defaultThumbUrl; |
224 | $this->hashLevels = $info['hashLevels'] ?? 2; |
225 | $this->deletedHashLevels = $info['deletedHashLevels'] ?? $this->hashLevels; |
226 | $this->transformVia404 = !empty( $info['transformVia404'] ); |
227 | $this->abbrvThreshold = $info['abbrvThreshold'] ?? 255; |
228 | $this->isPrivate = !empty( $info['isPrivate'] ); |
229 | // Give defaults for the basic zones... |
230 | $this->zones = $info['zones'] ?? []; |
231 | foreach ( [ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as $zone ) { |
232 | if ( !isset( $this->zones[$zone]['container'] ) ) { |
233 | $this->zones[$zone]['container'] = "{$this->name}-{$zone}"; |
234 | } |
235 | if ( !isset( $this->zones[$zone]['directory'] ) ) { |
236 | $this->zones[$zone]['directory'] = ''; |
237 | } |
238 | if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) { |
239 | $this->zones[$zone]['urlsByExt'] = []; |
240 | } |
241 | } |
242 | |
243 | $this->supportsSha1URLs = !empty( $info['supportsSha1URLs'] ); |
244 | |
245 | $this->wanCache = $info['wanCache'] ?? WANObjectCache::newEmpty(); |
246 | } |
247 | |
248 | /** |
249 | * Get the file backend instance. Use this function wisely. |
250 | * |
251 | * @return FileBackend |
252 | */ |
253 | public function getBackend() { |
254 | return $this->backend; |
255 | } |
256 | |
257 | /** |
258 | * Get an explanatory message if this repo is read-only. |
259 | * This checks if an administrator disabled writes to the backend. |
260 | * |
261 | * @return string|false Returns false if the repo is not read-only |
262 | */ |
263 | public function getReadOnlyReason() { |
264 | return $this->backend->getReadOnlyReason(); |
265 | } |
266 | |
267 | /** |
268 | * Ensure that a single zone or list of zones is defined for usage |
269 | * |
270 | * @param string[]|string $doZones Only do a particular zones |
271 | */ |
272 | protected function initZones( $doZones = [] ): void { |
273 | foreach ( (array)$doZones as $zone ) { |
274 | $root = $this->getZonePath( $zone ); |
275 | if ( $root === null ) { |
276 | throw new RuntimeException( "No '$zone' zone defined in the {$this->name} repo." ); |
277 | } |
278 | } |
279 | } |
280 | |
281 | /** |
282 | * Determine if a string is an mwrepo:// URL |
283 | * |
284 | * @param string $url |
285 | * @return bool |
286 | */ |
287 | public static function isVirtualUrl( $url ) { |
288 | return str_starts_with( $url, 'mwrepo://' ); |
289 | } |
290 | |
291 | /** |
292 | * Get a URL referring to this repository, with the private mwrepo protocol. |
293 | * The suffix, if supplied, is considered to be unencoded, and will be |
294 | * URL-encoded before being returned. |
295 | * |
296 | * @param string|false $suffix |
297 | * @return string |
298 | */ |
299 | public function getVirtualUrl( $suffix = false ) { |
300 | $path = 'mwrepo://' . $this->name; |
301 | if ( $suffix !== false ) { |
302 | $path .= '/' . rawurlencode( $suffix ); |
303 | } |
304 | |
305 | return $path; |
306 | } |
307 | |
308 | /** |
309 | * Get the URL corresponding to one of the four basic zones |
310 | * |
311 | * @param string $zone One of: public, deleted, temp, thumb |
312 | * @param string|null $ext Optional file extension |
313 | * @return string|false |
314 | */ |
315 | public function getZoneUrl( $zone, $ext = null ) { |
316 | if ( in_array( $zone, [ 'public', 'thumb', 'transcoded' ] ) ) { |
317 | // standard public zones |
318 | if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) { |
319 | // custom URL for extension/zone |
320 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable |
321 | return $this->zones[$zone]['urlsByExt'][$ext]; |
322 | } elseif ( isset( $this->zones[$zone]['url'] ) ) { |
323 | // custom URL for zone |
324 | return $this->zones[$zone]['url']; |
325 | } |
326 | } |
327 | switch ( $zone ) { |
328 | case 'public': |
329 | return $this->url; |
330 | case 'temp': |
331 | case 'deleted': |
332 | return false; // no public URL |
333 | case 'thumb': |
334 | return $this->thumbUrl; |
335 | case 'transcoded': |
336 | return "{$this->url}/transcoded"; |
337 | default: |
338 | return false; |
339 | } |
340 | } |
341 | |
342 | /** |
343 | * @return bool Whether non-ASCII path characters are allowed |
344 | */ |
345 | public function backendSupportsUnicodePaths() { |
346 | return (bool)( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS ); |
347 | } |
348 | |
349 | /** |
350 | * Get the backend storage path corresponding to a virtual URL. Callers are responsible of |
351 | * verifying that $url is a valid virtual URL. |
352 | * Use this function wisely. |
353 | * |
354 | * @param string $url |
355 | * @return string |
356 | */ |
357 | public function resolveVirtualUrl( $url ) { |
358 | if ( !str_starts_with( $url, 'mwrepo://' ) ) { |
359 | throw new InvalidArgumentException( __METHOD__ . ': unknown protocol' ); |
360 | } |
361 | $bits = explode( '/', substr( $url, 9 ), 3 ); |
362 | if ( count( $bits ) != 3 ) { |
363 | throw new InvalidArgumentException( __METHOD__ . ": invalid mwrepo URL: $url" ); |
364 | } |
365 | [ $repo, $zone, $rel ] = $bits; |
366 | if ( $repo !== $this->name ) { |
367 | throw new InvalidArgumentException( __METHOD__ . ": fetching from a foreign repo is not supported" ); |
368 | } |
369 | $base = $this->getZonePath( $zone ); |
370 | if ( !$base ) { |
371 | throw new InvalidArgumentException( __METHOD__ . ": invalid zone: $zone" ); |
372 | } |
373 | |
374 | return $base . '/' . rawurldecode( $rel ); |
375 | } |
376 | |
377 | /** |
378 | * The storage container and base path of a zone |
379 | * |
380 | * @param string $zone |
381 | * @return array (container, base path) or (null, null) |
382 | */ |
383 | protected function getZoneLocation( $zone ) { |
384 | if ( !isset( $this->zones[$zone] ) ) { |
385 | return [ null, null ]; // bogus |
386 | } |
387 | |
388 | return [ $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ]; |
389 | } |
390 | |
391 | /** |
392 | * Get the storage path corresponding to one of the zones |
393 | * |
394 | * @param string $zone |
395 | * @return string|null Returns null if the zone is not defined |
396 | */ |
397 | public function getZonePath( $zone ) { |
398 | [ $container, $base ] = $this->getZoneLocation( $zone ); |
399 | if ( $container === null || $base === null ) { |
400 | return null; |
401 | } |
402 | $backendName = $this->backend->getName(); |
403 | if ( $base != '' ) { // may not be set |
404 | $base = "/{$base}"; |
405 | } |
406 | |
407 | return "mwstore://$backendName/{$container}{$base}"; |
408 | } |
409 | |
410 | /** |
411 | * Create a new File object from the local repository |
412 | * |
413 | * @param PageIdentity|LinkTarget|string $title |
414 | * @param string|false $time Time at which the image was uploaded. If this |
415 | * is specified, the returned object will be an instance of the |
416 | * repository's old file class instead of a current file. Repositories |
417 | * not supporting version control should return false if this parameter |
418 | * is set. |
419 | * @return File|null A File, or null if passed an invalid Title |
420 | */ |
421 | public function newFile( $title, $time = false ) { |
422 | $title = File::normalizeTitle( $title ); |
423 | if ( !$title ) { |
424 | return null; |
425 | } |
426 | if ( $time ) { |
427 | if ( $this->oldFileFactory ) { |
428 | return call_user_func( $this->oldFileFactory, $title, $this, $time ); |
429 | } else { |
430 | return null; |
431 | } |
432 | } else { |
433 | return call_user_func( $this->fileFactory, $title, $this ); |
434 | } |
435 | } |
436 | |
437 | /** |
438 | * Find an instance of the named file created at the specified time |
439 | * Returns false if the file does not exist. Repositories not supporting |
440 | * version control should return false if the time is specified. |
441 | * |
442 | * @param PageIdentity|LinkTarget|string $title |
443 | * @param array $options Associative array of options: |
444 | * time: requested time for a specific file version, or false for the |
445 | * current version. An image object will be returned which was |
446 | * created at the specified time (which may be archived or current). |
447 | * ignoreRedirect: If true, do not follow file redirects |
448 | * private: If an Authority object, return restricted (deleted) files if the |
449 | * performer is allowed to view them. Otherwise, such files will not |
450 | * be found. If set and not an Authority object, throws an exception. |
451 | * Authority is only accepted since 1.37, User was required before. |
452 | * latest: If true, load from the latest available data into File objects |
453 | * @return File|false False on failure |
454 | * @throws InvalidArgumentException |
455 | */ |
456 | public function findFile( $title, $options = [] ) { |
457 | if ( !empty( $options['private'] ) && !( $options['private'] instanceof Authority ) ) { |
458 | throw new InvalidArgumentException( |
459 | __METHOD__ . ' called with the `private` option set to something ' . |
460 | 'other than an Authority object' |
461 | ); |
462 | } |
463 | |
464 | $title = File::normalizeTitle( $title ); |
465 | if ( !$title ) { |
466 | return false; |
467 | } |
468 | if ( isset( $options['bypassCache'] ) ) { |
469 | $options['latest'] = $options['bypassCache']; // b/c |
470 | } |
471 | $time = $options['time'] ?? false; |
472 | $flags = !empty( $options['latest'] ) ? IDBAccessObject::READ_LATEST : 0; |
473 | # First try the current version of the file to see if it precedes the timestamp |
474 | $img = $this->newFile( $title ); |
475 | if ( !$img ) { |
476 | return false; |
477 | } |
478 | $img->load( $flags ); |
479 | if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { |
480 | return $img; |
481 | } |
482 | # Now try an old version of the file |
483 | if ( $time !== false ) { |
484 | $img = $this->newFile( $title, $time ); |
485 | if ( $img ) { |
486 | $img->load( $flags ); |
487 | if ( $img->exists() ) { |
488 | if ( !$img->isDeleted( File::DELETED_FILE ) ) { |
489 | return $img; // always OK |
490 | } elseif ( |
491 | // If its not empty, its an Authority object |
492 | !empty( $options['private'] ) && |
493 | $img->userCan( File::DELETED_FILE, $options['private'] ) |
494 | ) { |
495 | return $img; |
496 | } |
497 | } |
498 | } |
499 | } |
500 | |
501 | # Now try redirects |
502 | if ( !empty( $options['ignoreRedirect'] ) ) { |
503 | return false; |
504 | } |
505 | $redir = $this->checkRedirect( $title ); |
506 | if ( $redir && $title->getNamespace() === NS_FILE ) { |
507 | $img = $this->newFile( $redir ); |
508 | if ( !$img ) { |
509 | return false; |
510 | } |
511 | $img->load( $flags ); |
512 | if ( $img->exists() ) { |
513 | $img->redirectedFrom( $title->getDBkey() ); |
514 | |
515 | return $img; |
516 | } |
517 | } |
518 | |
519 | return false; |
520 | } |
521 | |
522 | /** |
523 | * Find many files at once. |
524 | * |
525 | * @param array $items An array of titles, or an array of findFile() options with |
526 | * the "title" option giving the title. Example: |
527 | * |
528 | * $findItem = [ 'title' => $title, 'private' => true ]; |
529 | * $findBatch = [ $findItem ]; |
530 | * $repo->findFiles( $findBatch ); |
531 | * |
532 | * No title should appear in $items twice, as the result use titles as keys |
533 | * @param int $flags Supports: |
534 | * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map. |
535 | * The search title uses the input titles; the other is the final post-redirect title. |
536 | * All titles are returned as string DB keys and the inner array is associative. |
537 | * @return array Map of (file name => File objects) for matches or (search title => (title,timestamp)) |
538 | */ |
539 | public function findFiles( array $items, $flags = 0 ) { |
540 | $result = []; |
541 | foreach ( $items as $item ) { |
542 | if ( is_array( $item ) ) { |
543 | $title = $item['title']; |
544 | $options = $item; |
545 | unset( $options['title'] ); |
546 | |
547 | if ( |
548 | !empty( $options['private'] ) && |
549 | !( $options['private'] instanceof Authority ) |
550 | ) { |
551 | $options['private'] = RequestContext::getMain()->getAuthority(); |
552 | } |
553 | } else { |
554 | $title = $item; |
555 | $options = []; |
556 | } |
557 | $file = $this->findFile( $title, $options ); |
558 | if ( $file ) { |
559 | $searchName = File::normalizeTitle( $title )->getDBkey(); // must be valid |
560 | if ( $flags & self::NAME_AND_TIME_ONLY ) { |
561 | $result[$searchName] = [ |
562 | 'title' => $file->getTitle()->getDBkey(), |
563 | 'timestamp' => $file->getTimestamp() |
564 | ]; |
565 | } else { |
566 | $result[$searchName] = $file; |
567 | } |
568 | } |
569 | } |
570 | |
571 | return $result; |
572 | } |
573 | |
574 | /** |
575 | * Find an instance of the file with this key, created at the specified time |
576 | * Returns false if the file does not exist. Repositories not supporting |
577 | * version control should return false if the time is specified. |
578 | * |
579 | * @param string $sha1 Base 36 SHA-1 hash |
580 | * @param array $options Option array, same as findFile(). |
581 | * @return File|false False on failure |
582 | * @throws InvalidArgumentException if the `private` option is set and not an Authority object |
583 | */ |
584 | public function findFileFromKey( $sha1, $options = [] ) { |
585 | if ( !empty( $options['private'] ) && !( $options['private'] instanceof Authority ) ) { |
586 | throw new InvalidArgumentException( |
587 | __METHOD__ . ' called with the `private` option set to something ' . |
588 | 'other than an Authority object' |
589 | ); |
590 | } |
591 | |
592 | $time = $options['time'] ?? false; |
593 | # First try to find a matching current version of a file... |
594 | if ( !$this->fileFactoryKey ) { |
595 | return false; // find-by-sha1 not supported |
596 | } |
597 | $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); |
598 | if ( $img && $img->exists() ) { |
599 | return $img; |
600 | } |
601 | # Now try to find a matching old version of a file... |
602 | if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported? |
603 | $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); |
604 | if ( $img && $img->exists() ) { |
605 | if ( !$img->isDeleted( File::DELETED_FILE ) ) { |
606 | return $img; // always OK |
607 | } elseif ( |
608 | // If its not empty, its an Authority object |
609 | !empty( $options['private'] ) && |
610 | $img->userCan( File::DELETED_FILE, $options['private'] ) |
611 | ) { |
612 | return $img; |
613 | } |
614 | } |
615 | } |
616 | |
617 | return false; |
618 | } |
619 | |
620 | /** |
621 | * Get an array or iterator of file objects for files that have a given |
622 | * SHA-1 content hash. |
623 | * |
624 | * STUB |
625 | * @param string $hash SHA-1 hash |
626 | * @return File[] |
627 | */ |
628 | public function findBySha1( $hash ) { |
629 | return []; |
630 | } |
631 | |
632 | /** |
633 | * Get an array of arrays or iterators of file objects for files that |
634 | * have the given SHA-1 content hashes. |
635 | * |
636 | * @param string[] $hashes An array of hashes |
637 | * @return File[][] An Array of arrays or iterators of file objects and the hash as key |
638 | */ |
639 | public function findBySha1s( array $hashes ) { |
640 | $result = []; |
641 | foreach ( $hashes as $hash ) { |
642 | $files = $this->findBySha1( $hash ); |
643 | if ( count( $files ) ) { |
644 | $result[$hash] = $files; |
645 | } |
646 | } |
647 | |
648 | return $result; |
649 | } |
650 | |
651 | /** |
652 | * Return an array of files where the name starts with $prefix. |
653 | * |
654 | * STUB |
655 | * @param string $prefix The prefix to search for |
656 | * @param int $limit The maximum amount of files to return |
657 | * @return LocalFile[] |
658 | */ |
659 | public function findFilesByPrefix( $prefix, $limit ) { |
660 | return []; |
661 | } |
662 | |
663 | /** |
664 | * Get the URL of thumb.php |
665 | * |
666 | * @return string |
667 | */ |
668 | public function getThumbScriptUrl() { |
669 | return $this->thumbScriptUrl; |
670 | } |
671 | |
672 | /** |
673 | * Get the URL thumb.php requests are being proxied to |
674 | * |
675 | * @return string |
676 | */ |
677 | public function getThumbProxyUrl() { |
678 | return $this->thumbProxyUrl; |
679 | } |
680 | |
681 | /** |
682 | * Get the secret key for the proxied thumb service |
683 | * |
684 | * @return string |
685 | */ |
686 | public function getThumbProxySecret() { |
687 | return $this->thumbProxySecret; |
688 | } |
689 | |
690 | /** |
691 | * Returns true if the repository can transform files via a 404 handler |
692 | * |
693 | * @return bool |
694 | */ |
695 | public function canTransformVia404() { |
696 | return $this->transformVia404; |
697 | } |
698 | |
699 | /** |
700 | * Returns true if the repository can transform files locally. |
701 | * |
702 | * @since 1.36 |
703 | * @return bool |
704 | */ |
705 | public function canTransformLocally() { |
706 | return !$this->disableLocalTransform; |
707 | } |
708 | |
709 | /** |
710 | * Get the name of a file from its title |
711 | * |
712 | * @param PageIdentity|LinkTarget $title |
713 | * @return string |
714 | */ |
715 | public function getNameFromTitle( $title ) { |
716 | if ( |
717 | $this->initialCapital != |
718 | MediaWikiServices::getInstance()->getNamespaceInfo()->isCapitalized( NS_FILE ) |
719 | ) { |
720 | $name = $title->getDBkey(); |
721 | if ( $this->initialCapital ) { |
722 | $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name ); |
723 | } |
724 | } else { |
725 | $name = $title->getDBkey(); |
726 | } |
727 | |
728 | return $name; |
729 | } |
730 | |
731 | /** |
732 | * Get the public zone root storage directory of the repository |
733 | * |
734 | * @return string |
735 | */ |
736 | public function getRootDirectory() { |
737 | return $this->getZonePath( 'public' ); |
738 | } |
739 | |
740 | /** |
741 | * Get a relative path including trailing slash, e.g. f/fa/ |
742 | * If the repo is not hashed, returns an empty string |
743 | * |
744 | * @param string $name Name of file |
745 | * @return string |
746 | */ |
747 | public function getHashPath( $name ) { |
748 | return self::getHashPathForLevel( $name, $this->hashLevels ); |
749 | } |
750 | |
751 | /** |
752 | * Get a relative path including trailing slash, e.g. f/fa/ |
753 | * If the repo is not hashed, returns an empty string |
754 | * |
755 | * @param string $suffix Basename of file from FileRepo::storeTemp() |
756 | * @return string |
757 | */ |
758 | public function getTempHashPath( $suffix ) { |
759 | $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name> |
760 | $name = $parts[1] ?? $suffix; // hash path is not based on timestamp |
761 | return self::getHashPathForLevel( $name, $this->hashLevels ); |
762 | } |
763 | |
764 | /** |
765 | * @param string $name |
766 | * @param int $levels |
767 | * @return string |
768 | */ |
769 | protected static function getHashPathForLevel( $name, $levels ) { |
770 | if ( $levels == 0 ) { |
771 | return ''; |
772 | } else { |
773 | $hash = md5( $name ); |
774 | $path = ''; |
775 | for ( $i = 1; $i <= $levels; $i++ ) { |
776 | $path .= substr( $hash, 0, $i ) . '/'; |
777 | } |
778 | |
779 | return $path; |
780 | } |
781 | } |
782 | |
783 | /** |
784 | * Get the number of hash directory levels |
785 | * |
786 | * @return int |
787 | */ |
788 | public function getHashLevels() { |
789 | return $this->hashLevels; |
790 | } |
791 | |
792 | /** |
793 | * Get the name of this repository, as specified by $info['name]' to the constructor |
794 | * |
795 | * @return string |
796 | */ |
797 | public function getName() { |
798 | return $this->name; |
799 | } |
800 | |
801 | /** |
802 | * Make an url to this repo |
803 | * |
804 | * @param string|array $query Query string to append |
805 | * @param string $entry Entry point; defaults to index |
806 | * @return string|false False on failure |
807 | */ |
808 | public function makeUrl( $query = '', $entry = 'index' ) { |
809 | if ( isset( $this->scriptDirUrl ) ) { |
810 | return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}.php", $query ); |
811 | } |
812 | |
813 | return false; |
814 | } |
815 | |
816 | /** |
817 | * Get the URL of an image description page. May return false if it is |
818 | * unknown or not applicable. In general this should only be called by the |
819 | * File class, since it may return invalid results for certain kinds of |
820 | * repositories. Use File::getDescriptionUrl() in user code. |
821 | * |
822 | * In particular, it uses the article paths as specified to the repository |
823 | * constructor, whereas local repositories use the local Title functions. |
824 | * |
825 | * @param string $name |
826 | * @return string|false |
827 | */ |
828 | public function getDescriptionUrl( $name ) { |
829 | $encName = wfUrlencode( $name ); |
830 | if ( $this->descBaseUrl !== null ) { |
831 | # "http://example.com/wiki/File:" |
832 | return $this->descBaseUrl . $encName; |
833 | } |
834 | if ( $this->articleUrl !== null ) { |
835 | # "http://example.com/wiki/$1" |
836 | # We use "Image:" as the canonical namespace for |
837 | # compatibility across all MediaWiki versions. |
838 | return str_replace( '$1', |
839 | "Image:$encName", $this->articleUrl ); |
840 | } |
841 | if ( $this->scriptDirUrl !== null ) { |
842 | # "http://example.com/w" |
843 | # We use "Image:" as the canonical namespace for |
844 | # compatibility across all MediaWiki versions, |
845 | # and just sort of hope index.php is right. ;) |
846 | return $this->makeUrl( "title=Image:$encName" ); |
847 | } |
848 | |
849 | return false; |
850 | } |
851 | |
852 | /** |
853 | * Get the URL of the content-only fragment of the description page. For |
854 | * MediaWiki this means action=render. This should only be called by the |
855 | * repository's file class, since it may return invalid results. User code |
856 | * should use File::getDescriptionText(). |
857 | * |
858 | * @param string $name Name of image to fetch |
859 | * @param string|null $lang Language to fetch it in, if any. |
860 | * @return string|false |
861 | */ |
862 | public function getDescriptionRenderUrl( $name, $lang = null ) { |
863 | $query = 'action=render'; |
864 | if ( $lang !== null ) { |
865 | $query .= '&uselang=' . urlencode( $lang ); |
866 | } |
867 | if ( isset( $this->scriptDirUrl ) ) { |
868 | return $this->makeUrl( |
869 | 'title=' . |
870 | wfUrlencode( 'Image:' . $name ) . |
871 | "&$query" ); |
872 | } else { |
873 | $descUrl = $this->getDescriptionUrl( $name ); |
874 | if ( $descUrl ) { |
875 | return wfAppendQuery( $descUrl, $query ); |
876 | } else { |
877 | return false; |
878 | } |
879 | } |
880 | } |
881 | |
882 | /** |
883 | * Get the URL of the stylesheet to apply to description pages |
884 | * |
885 | * @return string|false False on failure |
886 | */ |
887 | public function getDescriptionStylesheetUrl() { |
888 | if ( isset( $this->scriptDirUrl ) ) { |
889 | // Must match canonical query parameter order for optimum caching |
890 | // See HTMLCacheUpdater::getUrls |
891 | return $this->makeUrl( 'title=MediaWiki:Filepage.css&action=raw&ctype=text/css' ); |
892 | } |
893 | |
894 | return false; |
895 | } |
896 | |
897 | /** |
898 | * Store a file to a given destination. |
899 | * |
900 | * Using FSFile/TempFSFile can improve performance via caching. |
901 | * Using TempFSFile can further improve performance by signalling that it is safe |
902 | * to touch the source file or write extended attribute metadata to it directly. |
903 | * |
904 | * @param string|FSFile $srcPath Source file system path, storage path, or virtual URL |
905 | * @param string $dstZone Destination zone |
906 | * @param string $dstRel Destination relative path |
907 | * @param int $flags Bitwise combination of the following flags: |
908 | * self::OVERWRITE Overwrite an existing destination file instead of failing |
909 | * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the |
910 | * same contents as the source |
911 | * self::SKIP_LOCKING Skip any file locking when doing the store |
912 | * @return Status |
913 | */ |
914 | public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { |
915 | $this->assertWritableRepo(); // fail out if read-only |
916 | |
917 | $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags ); |
918 | if ( $status->successCount == 0 ) { |
919 | $status->setOK( false ); |
920 | } |
921 | |
922 | return $status; |
923 | } |
924 | |
925 | /** |
926 | * Store a batch of files |
927 | * |
928 | * @see FileRepo::store() |
929 | * |
930 | * @param array $triplets (src, dest zone, dest rel) triplets as per store() |
931 | * @param int $flags Bitwise combination of the following flags: |
932 | * self::OVERWRITE Overwrite an existing destination file instead of failing |
933 | * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the |
934 | * same contents as the source |
935 | * self::SKIP_LOCKING Skip any file locking when doing the store |
936 | * @return Status |
937 | */ |
938 | public function storeBatch( array $triplets, $flags = 0 ) { |
939 | $this->assertWritableRepo(); // fail out if read-only |
940 | |
941 | if ( $flags & self::DELETE_SOURCE ) { |
942 | throw new InvalidArgumentException( "DELETE_SOURCE not supported in " . __METHOD__ ); |
943 | } |
944 | |
945 | $status = $this->newGood(); |
946 | $backend = $this->backend; // convenience |
947 | |
948 | $operations = []; |
949 | // Validate each triplet and get the store operation... |
950 | foreach ( $triplets as [ $src, $dstZone, $dstRel ] ) { |
951 | $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src; |
952 | wfDebug( __METHOD__ |
953 | . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )" |
954 | ); |
955 | // Resolve source path |
956 | if ( $src instanceof FSFile ) { |
957 | $op = 'store'; |
958 | } else { |
959 | $src = $this->resolveToStoragePathIfVirtual( $src ); |
960 | $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store'; |
961 | } |
962 | // Resolve destination path |
963 | $root = $this->getZonePath( $dstZone ); |
964 | if ( !$root ) { |
965 | throw new RuntimeException( "Invalid zone: $dstZone" ); |
966 | } |
967 | if ( !$this->validateFilename( $dstRel ) ) { |
968 | throw new RuntimeException( 'Validation error in $dstRel' ); |
969 | } |
970 | $dstPath = "$root/$dstRel"; |
971 | $dstDir = dirname( $dstPath ); |
972 | // Create destination directories for this triplet |
973 | if ( !$this->initDirectory( $dstDir )->isOK() ) { |
974 | return $this->newFatal( 'directorycreateerror', $dstDir ); |
975 | } |
976 | |
977 | // Copy the source file to the destination |
978 | $operations[] = [ |
979 | 'op' => $op, |
980 | 'src' => $src, // storage path (copy) or local file path (store) |
981 | 'dst' => $dstPath, |
982 | 'overwrite' => (bool)( $flags & self::OVERWRITE ), |
983 | 'overwriteSame' => (bool)( $flags & self::OVERWRITE_SAME ), |
984 | ]; |
985 | } |
986 | |
987 | // Execute the store operation for each triplet |
988 | $opts = [ 'force' => true ]; |
989 | if ( $flags & self::SKIP_LOCKING ) { |
990 | $opts['nonLocking'] = true; |
991 | } |
992 | |
993 | return $status->merge( $backend->doOperations( $operations, $opts ) ); |
994 | } |
995 | |
996 | /** |
997 | * Deletes a batch of files. |
998 | * Each file can be a (zone, rel) pair, virtual url, storage path. |
999 | * It will try to delete each file, but ignores any errors that may occur. |
1000 | * |
1001 | * @param string[] $files List of files to delete |
1002 | * @param int $flags Bitwise combination of the following flags: |
1003 | * self::SKIP_LOCKING Skip any file locking when doing the deletions |
1004 | * @return Status |
1005 | */ |
1006 | public function cleanupBatch( array $files, $flags = 0 ) { |
1007 | $this->assertWritableRepo(); // fail out if read-only |
1008 | |
1009 | $status = $this->newGood(); |
1010 | |
1011 | $operations = []; |
1012 | foreach ( $files as $path ) { |
1013 | if ( is_array( $path ) ) { |
1014 | // This is a pair, extract it |
1015 | [ $zone, $rel ] = $path; |
1016 | $path = $this->getZonePath( $zone ) . "/$rel"; |
1017 | } else { |
1018 | // Resolve source to a storage path if virtual |
1019 | $path = $this->resolveToStoragePathIfVirtual( $path ); |
1020 | } |
1021 | $operations[] = [ 'op' => 'delete', 'src' => $path ]; |
1022 | } |
1023 | // Actually delete files from storage... |
1024 | $opts = [ 'force' => true ]; |
1025 | if ( $flags & self::SKIP_LOCKING ) { |
1026 | $opts['nonLocking'] = true; |
1027 | } |
1028 | |
1029 | return $status->merge( $this->backend->doOperations( $operations, $opts ) ); |
1030 | } |
1031 | |
1032 | /** |
1033 | * Import a file from the local file system into the repo. |
1034 | * This does no locking and overrides existing files. |
1035 | * This function can be used to write to otherwise read-only foreign repos. |
1036 | * This is intended for copying generated thumbnails into the repo. |
1037 | * |
1038 | * Using FSFile/TempFSFile can improve performance via caching. |
1039 | * Using TempFSFile can further improve performance by signalling that it is safe |
1040 | * to touch the source file or write extended attribute metadata to it directly. |
1041 | * |
1042 | * @param string|FSFile $src Source file system path, storage path, or virtual URL |
1043 | * @param string $dst Virtual URL or storage path |
1044 | * @param array|string|null $options An array consisting of a key named headers |
1045 | * listing extra headers. If a string, taken as content-disposition header. |
1046 | * (Support for array of options new in 1.23) |
1047 | * @return Status |
1048 | */ |
1049 | final public function quickImport( $src, $dst, $options = null ) { |
1050 | return $this->quickImportBatch( [ [ $src, $dst, $options ] ] ); |
1051 | } |
1052 | |
1053 | /** |
1054 | * Import a batch of files from the local file system into the repo. |
1055 | * This does no locking and overrides existing files. |
1056 | * This function can be used to write to otherwise read-only foreign repos. |
1057 | * This is intended for copying generated thumbnails into the repo. |
1058 | * |
1059 | * @see FileRepo::quickImport() |
1060 | * |
1061 | * All path parameters may be a file system path, storage path, or virtual URL. |
1062 | * When "headers" are given they are used as HTTP headers if supported. |
1063 | * |
1064 | * @param array $triples List of (source path or FSFile, destination path, disposition) |
1065 | * @return Status |
1066 | */ |
1067 | public function quickImportBatch( array $triples ) { |
1068 | $status = $this->newGood(); |
1069 | $operations = []; |
1070 | foreach ( $triples as $triple ) { |
1071 | [ $src, $dst ] = $triple; |
1072 | if ( $src instanceof FSFile ) { |
1073 | $op = 'store'; |
1074 | } else { |
1075 | $src = $this->resolveToStoragePathIfVirtual( $src ); |
1076 | $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store'; |
1077 | } |
1078 | $dst = $this->resolveToStoragePathIfVirtual( $dst ); |
1079 | |
1080 | if ( !isset( $triple[2] ) ) { |
1081 | $headers = []; |
1082 | } elseif ( is_string( $triple[2] ) ) { |
1083 | // back-compat |
1084 | $headers = [ 'Content-Disposition' => $triple[2] ]; |
1085 | } elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) { |
1086 | $headers = $triple[2]['headers']; |
1087 | } else { |
1088 | $headers = []; |
1089 | } |
1090 | |
1091 | $operations[] = [ |
1092 | 'op' => $op, |
1093 | 'src' => $src, // storage path (copy) or local path/FSFile (store) |
1094 | 'dst' => $dst, |
1095 | 'headers' => $headers |
1096 | ]; |
1097 | $status->merge( $this->initDirectory( dirname( $dst ) ) ); |
1098 | } |
1099 | |
1100 | return $status->merge( $this->backend->doQuickOperations( $operations ) ); |
1101 | } |
1102 | |
1103 | /** |
1104 | * Purge a file from the repo. This does no locking. |
1105 | * This function can be used to write to otherwise read-only foreign repos. |
1106 | * This is intended for purging thumbnails. |
1107 | * |
1108 | * @param string $path Virtual URL or storage path |
1109 | * @return Status |
1110 | */ |
1111 | final public function quickPurge( $path ) { |
1112 | return $this->quickPurgeBatch( [ $path ] ); |
1113 | } |
1114 | |
1115 | /** |
1116 | * Deletes a directory if empty. |
1117 | * This function can be used to write to otherwise read-only foreign repos. |
1118 | * |
1119 | * @param string $dir Virtual URL (or storage path) of directory to clean |
1120 | * @return Status |
1121 | */ |
1122 | public function quickCleanDir( $dir ) { |
1123 | return $this->newGood()->merge( |
1124 | $this->backend->clean( |
1125 | [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] |
1126 | ) |
1127 | ); |
1128 | } |
1129 | |
1130 | /** |
1131 | * Purge a batch of files from the repo. |
1132 | * This function can be used to write to otherwise read-only foreign repos. |
1133 | * This does no locking and is intended for purging thumbnails. |
1134 | * |
1135 | * @param string[] $paths List of virtual URLs or storage paths |
1136 | * @return Status |
1137 | */ |
1138 | public function quickPurgeBatch( array $paths ) { |
1139 | $status = $this->newGood(); |
1140 | $operations = []; |
1141 | foreach ( $paths as $path ) { |
1142 | $operations[] = [ |
1143 | 'op' => 'delete', |
1144 | 'src' => $this->resolveToStoragePathIfVirtual( $path ), |
1145 | 'ignoreMissingSource' => true |
1146 | ]; |
1147 | } |
1148 | $status->merge( $this->backend->doQuickOperations( $operations ) ); |
1149 | |
1150 | return $status; |
1151 | } |
1152 | |
1153 | /** |
1154 | * Pick a random name in the temp zone and store a file to it. |
1155 | * Returns a Status object with the file Virtual URL in the value, |
1156 | * file can later be disposed using FileRepo::freeTemp(). |
1157 | * |
1158 | * @param string $originalName The base name of the file as specified |
1159 | * by the user. The file extension will be maintained. |
1160 | * @param string $srcPath The current location of the file. |
1161 | * @return Status Object with the URL in the value. |
1162 | */ |
1163 | public function storeTemp( $originalName, $srcPath ) { |
1164 | $this->assertWritableRepo(); // fail out if read-only |
1165 | |
1166 | $date = MWTimestamp::getInstance()->format( 'YmdHis' ); |
1167 | $hashPath = $this->getHashPath( $originalName ); |
1168 | $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName ); |
1169 | $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel; |
1170 | |
1171 | $result = $this->quickImport( $srcPath, $virtualUrl ); |
1172 | $result->value = $virtualUrl; |
1173 | |
1174 | return $result; |
1175 | } |
1176 | |
1177 | /** |
1178 | * Remove a temporary file or mark it for garbage collection |
1179 | * |
1180 | * @param string $virtualUrl The virtual URL returned by FileRepo::storeTemp() |
1181 | * @return bool True on success, false on failure |
1182 | */ |
1183 | public function freeTemp( $virtualUrl ) { |
1184 | $this->assertWritableRepo(); // fail out if read-only |
1185 | |
1186 | $temp = $this->getVirtualUrl( 'temp' ); |
1187 | if ( !str_starts_with( $virtualUrl, $temp ) ) { |
1188 | wfDebug( __METHOD__ . ": Invalid temp virtual URL" ); |
1189 | |
1190 | return false; |
1191 | } |
1192 | |
1193 | return $this->quickPurge( $virtualUrl )->isOK(); |
1194 | } |
1195 | |
1196 | /** |
1197 | * Concatenate a list of temporary files into a target file location. |
1198 | * |
1199 | * @param string[] $srcPaths Ordered list of source virtual URLs/storage paths |
1200 | * @param string $dstPath Target file system path |
1201 | * @param int $flags Bitwise combination of the following flags: |
1202 | * self::DELETE_SOURCE Delete the source files on success |
1203 | * @return Status |
1204 | */ |
1205 | public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) { |
1206 | $this->assertWritableRepo(); // fail out if read-only |
1207 | |
1208 | $status = $this->newGood(); |
1209 | |
1210 | $sources = []; |
1211 | foreach ( $srcPaths as $srcPath ) { |
1212 | // Resolve source to a storage path if virtual |
1213 | $source = $this->resolveToStoragePathIfVirtual( $srcPath ); |
1214 | $sources[] = $source; // chunk to merge |
1215 | } |
1216 | |
1217 | // Concatenate the chunks into one FS file |
1218 | $params = [ 'srcs' => $sources, 'dst' => $dstPath ]; |
1219 | $status->merge( $this->backend->concatenate( $params ) ); |
1220 | if ( !$status->isOK() ) { |
1221 | return $status; |
1222 | } |
1223 | |
1224 | // Delete the sources if required |
1225 | if ( $flags & self::DELETE_SOURCE ) { |
1226 | $status->merge( $this->quickPurgeBatch( $srcPaths ) ); |
1227 | } |
1228 | |
1229 | // Make sure status is OK, despite any quickPurgeBatch() fatals |
1230 | $status->setResult( true ); |
1231 | |
1232 | return $status; |
1233 | } |
1234 | |
1235 | /** |
1236 | * Copy or move a file either from a storage path, virtual URL, |
1237 | * or file system path, into this repository at the specified destination location. |
1238 | * |
1239 | * Returns a Status object. On success, the value contains "new" or |
1240 | * "archived", to indicate whether the file was new with that name. |
1241 | * |
1242 | * Using FSFile/TempFSFile can improve performance via caching. |
1243 | * Using TempFSFile can further improve performance by signalling that it is safe |
1244 | * to touch the source file or write extended attribute metadata to it directly. |
1245 | * |
1246 | * Options to $options include: |
1247 | * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests |
1248 | * |
1249 | * @param string|FSFile $src The source file system path, storage path, or URL |
1250 | * @param string $dstRel The destination relative path |
1251 | * @param string $archiveRel The relative path where the existing file is to |
1252 | * be archived, if there is one. Relative to the public zone root. |
1253 | * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate |
1254 | * that the source file should be deleted if possible |
1255 | * @param array $options Optional additional parameters |
1256 | * @return Status |
1257 | */ |
1258 | public function publish( |
1259 | $src, $dstRel, $archiveRel, $flags = 0, array $options = [] |
1260 | ) { |
1261 | $this->assertWritableRepo(); // fail out if read-only |
1262 | |
1263 | $status = $this->publishBatch( |
1264 | [ [ $src, $dstRel, $archiveRel, $options ] ], $flags ); |
1265 | if ( $status->successCount == 0 ) { |
1266 | $status->setOK( false ); |
1267 | } |
1268 | $status->value = $status->value[0] ?? false; |
1269 | |
1270 | return $status; |
1271 | } |
1272 | |
1273 | /** |
1274 | * Publish a batch of files |
1275 | * |
1276 | * @see FileRepo::publish() |
1277 | * |
1278 | * @param array $ntuples (source, dest, archive) triplets or |
1279 | * (source, dest, archive, options) 4-tuples as per publish(). |
1280 | * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate |
1281 | * that the source files should be deleted if possible |
1282 | * @return Status |
1283 | */ |
1284 | public function publishBatch( array $ntuples, $flags = 0 ) { |
1285 | $this->assertWritableRepo(); // fail out if read-only |
1286 | |
1287 | $backend = $this->backend; // convenience |
1288 | // Try creating directories |
1289 | $this->initZones( 'public' ); |
1290 | |
1291 | $status = $this->newGood( [] ); |
1292 | |
1293 | $operations = []; |
1294 | $sourceFSFilesToDelete = []; // cleanup for disk source files |
1295 | // Validate each triplet and get the store operation... |
1296 | foreach ( $ntuples as $ntuple ) { |
1297 | [ $src, $dstRel, $archiveRel ] = $ntuple; |
1298 | $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src; |
1299 | |
1300 | $options = $ntuple[3] ?? []; |
1301 | // Resolve source to a storage path if virtual |
1302 | $srcPath = $this->resolveToStoragePathIfVirtual( $srcPath ); |
1303 | if ( !$this->validateFilename( $dstRel ) ) { |
1304 | throw new RuntimeException( 'Validation error in $dstRel' ); |
1305 | } |
1306 | if ( !$this->validateFilename( $archiveRel ) ) { |
1307 | throw new RuntimeException( 'Validation error in $archiveRel' ); |
1308 | } |
1309 | |
1310 | $publicRoot = $this->getZonePath( 'public' ); |
1311 | $dstPath = "$publicRoot/$dstRel"; |
1312 | $archivePath = "$publicRoot/$archiveRel"; |
1313 | |
1314 | $dstDir = dirname( $dstPath ); |
1315 | $archiveDir = dirname( $archivePath ); |
1316 | // Abort immediately on directory creation errors since they're likely to be repetitive |
1317 | if ( !$this->initDirectory( $dstDir )->isOK() ) { |
1318 | return $this->newFatal( 'directorycreateerror', $dstDir ); |
1319 | } |
1320 | if ( !$this->initDirectory( $archiveDir )->isOK() ) { |
1321 | return $this->newFatal( 'directorycreateerror', $archiveDir ); |
1322 | } |
1323 | |
1324 | // Set any desired headers to be use in GET/HEAD responses |
1325 | $headers = $options['headers'] ?? []; |
1326 | |
1327 | // Archive destination file if it exists. |
1328 | // This will check if the archive file also exists and fail if does. |
1329 | // This is a check to avoid data loss. On Windows and Linux, |
1330 | // copy() will overwrite, so the existence check is vulnerable to |
1331 | // race conditions unless a functioning LockManager is used. |
1332 | // LocalFile also uses SELECT FOR UPDATE for synchronization. |
1333 | $operations[] = [ |
1334 | 'op' => 'copy', |
1335 | 'src' => $dstPath, |
1336 | 'dst' => $archivePath, |
1337 | 'ignoreMissingSource' => true |
1338 | ]; |
1339 | |
1340 | // Copy (or move) the source file to the destination |
1341 | if ( FileBackend::isStoragePath( $srcPath ) ) { |
1342 | $operations[] = [ |
1343 | 'op' => ( $flags & self::DELETE_SOURCE ) ? 'move' : 'copy', |
1344 | 'src' => $srcPath, |
1345 | 'dst' => $dstPath, |
1346 | 'overwrite' => true, // replace current |
1347 | 'headers' => $headers |
1348 | ]; |
1349 | } else { |
1350 | $operations[] = [ |
1351 | 'op' => 'store', |
1352 | 'src' => $src, // storage path (copy) or local path/FSFile (store) |
1353 | 'dst' => $dstPath, |
1354 | 'overwrite' => true, // replace current |
1355 | 'headers' => $headers |
1356 | ]; |
1357 | if ( $flags & self::DELETE_SOURCE ) { |
1358 | $sourceFSFilesToDelete[] = $srcPath; |
1359 | } |
1360 | } |
1361 | } |
1362 | |
1363 | // Execute the operations for each triplet |
1364 | $status->merge( $backend->doOperations( $operations ) ); |
1365 | // Find out which files were archived... |
1366 | foreach ( $ntuples as $i => $ntuple ) { |
1367 | [ , , $archiveRel ] = $ntuple; |
1368 | $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel"; |
1369 | if ( $this->fileExists( $archivePath ) ) { |
1370 | $status->value[$i] = 'archived'; |
1371 | } else { |
1372 | $status->value[$i] = 'new'; |
1373 | } |
1374 | } |
1375 | // Cleanup for disk source files... |
1376 | foreach ( $sourceFSFilesToDelete as $file ) { |
1377 | AtEase::suppressWarnings(); |
1378 | unlink( $file ); // FS cleanup |
1379 | AtEase::restoreWarnings(); |
1380 | } |
1381 | |
1382 | return $status; |
1383 | } |
1384 | |
1385 | /** |
1386 | * Creates a directory with the appropriate zone permissions. |
1387 | * Callers are responsible for doing read-only and "writable repo" checks. |
1388 | * |
1389 | * @param string $dir Virtual URL (or storage path) of directory to clean |
1390 | * @return Status Good status without value for success, fatal otherwise. |
1391 | */ |
1392 | protected function initDirectory( $dir ) { |
1393 | $path = $this->resolveToStoragePathIfVirtual( $dir ); |
1394 | [ , $container, ] = FileBackend::splitStoragePath( $path ); |
1395 | |
1396 | $params = [ 'dir' => $path ]; |
1397 | if ( $this->isPrivate |
1398 | || $container === $this->zones['deleted']['container'] |
1399 | || $container === $this->zones['temp']['container'] |
1400 | ) { |
1401 | # Take all available measures to prevent web accessibility of new deleted |
1402 | # directories, in case the user has not configured offline storage |
1403 | $params = [ 'noAccess' => true, 'noListing' => true ] + $params; |
1404 | } |
1405 | |
1406 | return $this->newGood()->merge( $this->backend->prepare( $params ) ); |
1407 | } |
1408 | |
1409 | /** |
1410 | * Deletes a directory if empty. |
1411 | * |
1412 | * @param string $dir Virtual URL (or storage path) of directory to clean |
1413 | * @return Status |
1414 | */ |
1415 | public function cleanDir( $dir ) { |
1416 | $this->assertWritableRepo(); // fail out if read-only |
1417 | |
1418 | return $this->newGood()->merge( |
1419 | $this->backend->clean( |
1420 | [ 'dir' => $this->resolveToStoragePathIfVirtual( $dir ) ] |
1421 | ) |
1422 | ); |
1423 | } |
1424 | |
1425 | /** |
1426 | * Checks existence of a file |
1427 | * |
1428 | * @param string $file Virtual URL (or storage path) of file to check |
1429 | * @return bool|null Whether the file exists, or null in case of I/O errors |
1430 | */ |
1431 | public function fileExists( $file ) { |
1432 | $result = $this->fileExistsBatch( [ $file ] ); |
1433 | |
1434 | return $result[0]; |
1435 | } |
1436 | |
1437 | /** |
1438 | * Checks existence of an array of files. |
1439 | * |
1440 | * @param string[] $files Virtual URLs (or storage paths) of files to check |
1441 | * @return array<string|int,bool|null> Map of files and either bool indicating whether the files exist, |
1442 | * or null in case of I/O errors |
1443 | */ |
1444 | public function fileExistsBatch( array $files ) { |
1445 | $paths = array_map( [ $this, 'resolveToStoragePathIfVirtual' ], $files ); |
1446 | $this->backend->preloadFileStat( [ 'srcs' => $paths ] ); |
1447 | |
1448 | $result = []; |
1449 | foreach ( $paths as $key => $path ) { |
1450 | $result[$key] = $this->backend->fileExists( [ 'src' => $path ] ); |
1451 | } |
1452 | |
1453 | return $result; |
1454 | } |
1455 | |
1456 | /** |
1457 | * Move a file to the deletion archive. |
1458 | * If no valid deletion archive exists, this may either delete the file |
1459 | * or throw an exception, depending on the preference of the repository |
1460 | * |
1461 | * @param mixed $srcRel Relative path for the file to be deleted |
1462 | * @param mixed $archiveRel Relative path for the archive location. |
1463 | * Relative to a private archive directory. |
1464 | * @return Status |
1465 | */ |
1466 | public function delete( $srcRel, $archiveRel ) { |
1467 | $this->assertWritableRepo(); // fail out if read-only |
1468 | |
1469 | return $this->deleteBatch( [ [ $srcRel, $archiveRel ] ] ); |
1470 | } |
1471 | |
1472 | /** |
1473 | * Move a group of files to the deletion archive. |
1474 | * |
1475 | * If no valid deletion archive is configured, this may either delete the |
1476 | * file or throw an exception, depending on the preference of the repository. |
1477 | * |
1478 | * The overwrite policy is determined by the repository -- currently LocalRepo |
1479 | * assumes a naming scheme in the deleted zone based on content hash, as |
1480 | * opposed to the public zone which is assumed to be unique. |
1481 | * |
1482 | * @param array $sourceDestPairs Array of source/destination pairs. Each element |
1483 | * is a two-element array containing the source file path relative to the |
1484 | * public root in the first element, and the archive file path relative |
1485 | * to the deleted zone root in the second element. |
1486 | * @return Status |
1487 | */ |
1488 | public function deleteBatch( array $sourceDestPairs ) { |
1489 | $this->assertWritableRepo(); // fail out if read-only |
1490 | |
1491 | // Try creating directories |
1492 | $this->initZones( [ 'public', 'deleted' ] ); |
1493 | |
1494 | $status = $this->newGood(); |
1495 | |
1496 | $backend = $this->backend; // convenience |
1497 | $operations = []; |
1498 | // Validate filenames and create archive directories |
1499 | foreach ( $sourceDestPairs as [ $srcRel, $archiveRel ] ) { |
1500 | if ( !$this->validateFilename( $srcRel ) ) { |
1501 | throw new RuntimeException( __METHOD__ . ':Validation error in $srcRel' ); |
1502 | } elseif ( !$this->validateFilename( $archiveRel ) ) { |
1503 | throw new RuntimeException( __METHOD__ . ':Validation error in $archiveRel' ); |
1504 | } |
1505 | |
1506 | $publicRoot = $this->getZonePath( 'public' ); |
1507 | $srcPath = "{$publicRoot}/$srcRel"; |
1508 | |
1509 | $deletedRoot = $this->getZonePath( 'deleted' ); |
1510 | $archivePath = "{$deletedRoot}/{$archiveRel}"; |
1511 | $archiveDir = dirname( $archivePath ); // does not touch FS |
1512 | |
1513 | // Create destination directories |
1514 | if ( !$this->initDirectory( $archiveDir )->isGood() ) { |
1515 | return $this->newFatal( 'directorycreateerror', $archiveDir ); |
1516 | } |
1517 | |
1518 | $operations[] = [ |
1519 | 'op' => 'move', |
1520 | 'src' => $srcPath, |
1521 | 'dst' => $archivePath, |
1522 | // We may have 2+ identical files being deleted, |
1523 | // all of which will map to the same destination file |
1524 | 'overwriteSame' => true // also see T33792 |
1525 | ]; |
1526 | } |
1527 | |
1528 | // Move the files by execute the operations for each pair. |
1529 | // We're now committed to returning an OK result, which will |
1530 | // lead to the files being moved in the DB also. |
1531 | $opts = [ 'force' => true ]; |
1532 | return $status->merge( $backend->doOperations( $operations, $opts ) ); |
1533 | } |
1534 | |
1535 | /** |
1536 | * Delete files in the deleted directory if they are not referenced in the filearchive table |
1537 | * |
1538 | * STUB |
1539 | * @param string[] $storageKeys |
1540 | */ |
1541 | public function cleanupDeletedBatch( array $storageKeys ) { |
1542 | $this->assertWritableRepo(); |
1543 | } |
1544 | |
1545 | /** |
1546 | * Get a relative path for a deletion archive key, |
1547 | * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg |
1548 | * |
1549 | * @param string $key |
1550 | * @return string |
1551 | */ |
1552 | public function getDeletedHashPath( $key ) { |
1553 | if ( strlen( $key ) < 31 ) { |
1554 | throw new InvalidArgumentException( "Invalid storage key '$key'." ); |
1555 | } |
1556 | $path = ''; |
1557 | for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { |
1558 | $path .= $key[$i] . '/'; |
1559 | } |
1560 | |
1561 | return $path; |
1562 | } |
1563 | |
1564 | /** |
1565 | * If a path is a virtual URL, resolve it to a storage path. |
1566 | * Otherwise, just return the path as it is. |
1567 | * |
1568 | * @param string $path |
1569 | * @return string |
1570 | */ |
1571 | protected function resolveToStoragePathIfVirtual( $path ) { |
1572 | if ( self::isVirtualUrl( $path ) ) { |
1573 | return $this->resolveVirtualUrl( $path ); |
1574 | } |
1575 | |
1576 | return $path; |
1577 | } |
1578 | |
1579 | /** |
1580 | * Get a local FS copy of a file with a given virtual URL/storage path. |
1581 | * Temporary files may be purged when the file object falls out of scope. |
1582 | * |
1583 | * @param string $virtualUrl |
1584 | * @return TempFSFile|null|false Returns false for missing file, null on failure |
1585 | */ |
1586 | public function getLocalCopy( $virtualUrl ) { |
1587 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1588 | |
1589 | return $this->backend->getLocalCopy( [ 'src' => $path ] ); |
1590 | } |
1591 | |
1592 | /** |
1593 | * Get a local FS file with a given virtual URL/storage path. |
1594 | * The file is either an original or a copy. It should not be changed. |
1595 | * Temporary files may be purged when the file object falls out of scope. |
1596 | * |
1597 | * @param string $virtualUrl |
1598 | * @return FSFile|null|false Returns false for missing file, null on failure. |
1599 | */ |
1600 | public function getLocalReference( $virtualUrl ) { |
1601 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1602 | |
1603 | return $this->backend->getLocalReference( [ 'src' => $path ] ); |
1604 | } |
1605 | |
1606 | /** |
1607 | * Get properties of a file with a given virtual URL/storage path. |
1608 | * Properties should ultimately be obtained via FSFile::getProps(). |
1609 | * |
1610 | * @param string $virtualUrl |
1611 | * @return array |
1612 | */ |
1613 | public function getFileProps( $virtualUrl ) { |
1614 | $fsFile = $this->getLocalReference( $virtualUrl ); |
1615 | $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() ); |
1616 | if ( $fsFile ) { |
1617 | $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true ); |
1618 | } else { |
1619 | $props = $mwProps->newPlaceholderProps(); |
1620 | } |
1621 | |
1622 | return $props; |
1623 | } |
1624 | |
1625 | /** |
1626 | * Get the timestamp of a file with a given virtual URL/storage path |
1627 | * |
1628 | * @param string $virtualUrl |
1629 | * @return string|false False on failure |
1630 | */ |
1631 | public function getFileTimestamp( $virtualUrl ) { |
1632 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1633 | |
1634 | return $this->backend->getFileTimestamp( [ 'src' => $path ] ); |
1635 | } |
1636 | |
1637 | /** |
1638 | * Get the size of a file with a given virtual URL/storage path |
1639 | * |
1640 | * @param string $virtualUrl |
1641 | * @return int|false |
1642 | */ |
1643 | public function getFileSize( $virtualUrl ) { |
1644 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1645 | |
1646 | return $this->backend->getFileSize( [ 'src' => $path ] ); |
1647 | } |
1648 | |
1649 | /** |
1650 | * Get the sha1 (base 36) of a file with a given virtual URL/storage path |
1651 | * |
1652 | * @param string $virtualUrl |
1653 | * @return string|false |
1654 | */ |
1655 | public function getFileSha1( $virtualUrl ) { |
1656 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1657 | |
1658 | return $this->backend->getFileSha1Base36( [ 'src' => $path ] ); |
1659 | } |
1660 | |
1661 | /** |
1662 | * Attempt to stream a file with the given virtual URL/storage path |
1663 | * |
1664 | * @param string $virtualUrl |
1665 | * @param array $headers Additional HTTP headers to send on success |
1666 | * @param array $optHeaders HTTP request headers (if-modified-since, range, ...) |
1667 | * @return Status |
1668 | * @since 1.27 |
1669 | */ |
1670 | public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) { |
1671 | $path = $this->resolveToStoragePathIfVirtual( $virtualUrl ); |
1672 | $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ]; |
1673 | |
1674 | // T172851: HHVM does not flush the output properly, causing OOM |
1675 | ob_start( null, 1_048_576 ); |
1676 | ob_implicit_flush( true ); |
1677 | |
1678 | $status = $this->newGood()->merge( $this->backend->streamFile( $params ) ); |
1679 | |
1680 | // T186565: Close the buffer, unless it has already been closed |
1681 | // in HTTPFileStreamer::resetOutputBuffers(). |
1682 | if ( ob_get_status() ) { |
1683 | ob_end_flush(); |
1684 | } |
1685 | |
1686 | return $status; |
1687 | } |
1688 | |
1689 | /** |
1690 | * Call a callback function for every public regular file in the repository. |
1691 | * This only acts on the current version of files, not any old versions. |
1692 | * May use either the database or the filesystem. |
1693 | * |
1694 | * @param callable $callback |
1695 | * @return void |
1696 | */ |
1697 | public function enumFiles( $callback ) { |
1698 | $this->enumFilesInStorage( $callback ); |
1699 | } |
1700 | |
1701 | /** |
1702 | * Call a callback function for every public file in the repository. |
1703 | * May use either the database or the filesystem. |
1704 | * |
1705 | * @param callable $callback |
1706 | * @return void |
1707 | */ |
1708 | protected function enumFilesInStorage( $callback ) { |
1709 | $publicRoot = $this->getZonePath( 'public' ); |
1710 | $numDirs = 1 << ( $this->hashLevels * 4 ); |
1711 | // Use a priori assumptions about directory structure |
1712 | // to reduce the tree height of the scanning process. |
1713 | for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) { |
1714 | $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex ); |
1715 | $path = $publicRoot; |
1716 | for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) { |
1717 | $path .= '/' . substr( $hexString, 0, $hexPos + 1 ); |
1718 | } |
1719 | $iterator = $this->backend->getFileList( [ 'dir' => $path ] ); |
1720 | if ( $iterator === null ) { |
1721 | throw new RuntimeException( __METHOD__ . ': could not get file listing for ' . $path ); |
1722 | } |
1723 | foreach ( $iterator as $name ) { |
1724 | // Each item returned is a public file |
1725 | call_user_func( $callback, "{$path}/{$name}" ); |
1726 | } |
1727 | } |
1728 | } |
1729 | |
1730 | /** |
1731 | * Determine if a relative path is valid, i.e. not blank or involving directory traversal |
1732 | * |
1733 | * @param string $filename |
1734 | * @return bool |
1735 | */ |
1736 | public function validateFilename( $filename ) { |
1737 | if ( strval( $filename ) == '' ) { |
1738 | return false; |
1739 | } |
1740 | |
1741 | return FileBackend::isPathTraversalFree( $filename ); |
1742 | } |
1743 | |
1744 | /** |
1745 | * Get a callback function to use for cleaning error message parameters |
1746 | * |
1747 | * @return callable |
1748 | */ |
1749 | private function getErrorCleanupFunction() { |
1750 | switch ( $this->pathDisclosureProtection ) { |
1751 | case 'none': |
1752 | case 'simple': // b/c |
1753 | $callback = [ $this, 'passThrough' ]; |
1754 | break; |
1755 | default: // 'paranoid' |
1756 | $callback = [ $this, 'paranoidClean' ]; |
1757 | } |
1758 | return $callback; |
1759 | } |
1760 | |
1761 | /** |
1762 | * Path disclosure protection function |
1763 | * |
1764 | * @param string $param |
1765 | * @return string |
1766 | */ |
1767 | public function paranoidClean( $param ) { |
1768 | return '[hidden]'; |
1769 | } |
1770 | |
1771 | /** |
1772 | * Path disclosure protection function |
1773 | * |
1774 | * @param string $param |
1775 | * @return string |
1776 | */ |
1777 | public function passThrough( $param ) { |
1778 | return $param; |
1779 | } |
1780 | |
1781 | /** |
1782 | * Create a new fatal error |
1783 | * |
1784 | * @param string $message |
1785 | * @param mixed ...$parameters |
1786 | * @return Status |
1787 | */ |
1788 | public function newFatal( $message, ...$parameters ) { |
1789 | $status = Status::newFatal( $message, ...$parameters ); |
1790 | $status->cleanCallback = $this->getErrorCleanupFunction(); |
1791 | |
1792 | return $status; |
1793 | } |
1794 | |
1795 | /** |
1796 | * Create a new good result |
1797 | * |
1798 | * @param null|mixed $value |
1799 | * @return Status |
1800 | */ |
1801 | public function newGood( $value = null ) { |
1802 | $status = Status::newGood( $value ); |
1803 | $status->cleanCallback = $this->getErrorCleanupFunction(); |
1804 | |
1805 | return $status; |
1806 | } |
1807 | |
1808 | /** |
1809 | * Checks if there is a redirect named as $title. If there is, return the |
1810 | * title object. If not, return false. |
1811 | * STUB |
1812 | * |
1813 | * @param PageIdentity|LinkTarget $title Title of image |
1814 | * @return Title|false |
1815 | */ |
1816 | public function checkRedirect( $title ) { |
1817 | return false; |
1818 | } |
1819 | |
1820 | /** |
1821 | * Invalidates image redirect cache related to that image |
1822 | * Doesn't do anything for repositories that don't support image redirects. |
1823 | * |
1824 | * STUB |
1825 | * @param PageIdentity|LinkTarget $title Title of image |
1826 | */ |
1827 | public function invalidateImageRedirect( $title ) { |
1828 | } |
1829 | |
1830 | /** |
1831 | * Get the human-readable name of the repo |
1832 | * |
1833 | * @return string |
1834 | */ |
1835 | public function getDisplayName() { |
1836 | $sitename = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Sitename ); |
1837 | |
1838 | if ( $this->isLocal() ) { |
1839 | return $sitename; |
1840 | } |
1841 | |
1842 | // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true |
1843 | return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); |
1844 | } |
1845 | |
1846 | /** |
1847 | * Get the portion of the file that contains the origin file name. |
1848 | * If that name is too long, then the name "thumbnail.<ext>" will be given. |
1849 | * |
1850 | * @param string $name |
1851 | * @return string |
1852 | */ |
1853 | public function nameForThumb( $name ) { |
1854 | if ( strlen( $name ) > $this->abbrvThreshold ) { |
1855 | $ext = FileBackend::extensionFromPath( $name ); |
1856 | $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext"; |
1857 | } |
1858 | |
1859 | return $name; |
1860 | } |
1861 | |
1862 | /** |
1863 | * Returns true if this the local file repository. |
1864 | * |
1865 | * @return bool |
1866 | */ |
1867 | public function isLocal() { |
1868 | return $this->getName() == 'local'; |
1869 | } |
1870 | |
1871 | /** |
1872 | * Get a global, repository-qualified, WAN cache key |
1873 | * |
1874 | * This might be called from either the site context of the wiki that owns the repo or |
1875 | * the site context of another wiki that simply has access to the repo. This returns |
1876 | * false if the repository's cache is not accessible from the current site context. |
1877 | * |
1878 | * @param string $kClassSuffix Key collection name suffix (added to this repo class) |
1879 | * @param mixed ...$components Additional key components |
1880 | * @return string|false |
1881 | */ |
1882 | public function getSharedCacheKey( $kClassSuffix, ...$components ) { |
1883 | return false; |
1884 | } |
1885 | |
1886 | /** |
1887 | * Get a site-local, repository-qualified, WAN cache key |
1888 | * |
1889 | * These cache keys are not shared among different site context and thus cannot be |
1890 | * directly invalidated when repo objects are modified. These are useful when there |
1891 | * is no accessible global cache or the values depend on the current site context. |
1892 | * |
1893 | * @param string $kClassSuffix Key collection name suffix (added to this repo class) |
1894 | * @param mixed ...$components Additional key components |
1895 | * @return string |
1896 | */ |
1897 | public function getLocalCacheKey( $kClassSuffix, ...$components ) { |
1898 | return $this->wanCache->makeKey( |
1899 | 'filerepo-' . $kClassSuffix, |
1900 | $this->getName(), |
1901 | ...$components |
1902 | ); |
1903 | } |
1904 | |
1905 | /** |
1906 | * Get a temporary private FileRepo associated with this repo. |
1907 | * |
1908 | * Files will be created in the temp zone of this repo. |
1909 | * It will have the same backend as this repo. |
1910 | * |
1911 | * @return TempFileRepo |
1912 | */ |
1913 | public function getTempRepo() { |
1914 | return new TempFileRepo( [ |
1915 | 'name' => "{$this->name}-temp", |
1916 | 'backend' => $this->backend, |
1917 | 'zones' => [ |
1918 | 'public' => [ |
1919 | // Same place storeTemp() uses in the base repo, though |
1920 | // the path hashing is mismatched, which is annoying. |
1921 | 'container' => $this->zones['temp']['container'], |
1922 | 'directory' => $this->zones['temp']['directory'] |
1923 | ], |
1924 | 'thumb' => [ |
1925 | 'container' => $this->zones['temp']['container'], |
1926 | 'directory' => $this->zones['temp']['directory'] == '' |
1927 | ? 'thumb' |
1928 | : $this->zones['temp']['directory'] . '/thumb' |
1929 | ], |
1930 | 'transcoded' => [ |
1931 | 'container' => $this->zones['temp']['container'], |
1932 | 'directory' => $this->zones['temp']['directory'] == '' |
1933 | ? 'transcoded' |
1934 | : $this->zones['temp']['directory'] . '/transcoded' |
1935 | ] |
1936 | ], |
1937 | 'hashLevels' => $this->hashLevels, // performance |
1938 | 'isPrivate' => true // all in temp zone |
1939 | ] ); |
1940 | } |
1941 | |
1942 | /** |
1943 | * Get an UploadStash associated with this repo. |
1944 | * |
1945 | * @param UserIdentity|null $user |
1946 | * @return UploadStash |
1947 | */ |
1948 | public function getUploadStash( UserIdentity $user = null ) { |
1949 | return new UploadStash( $this, $user ); |
1950 | } |
1951 | |
1952 | /** |
1953 | * Throw an exception if this repo is read-only by design. |
1954 | * This does not and should not check getReadOnlyReason(). |
1955 | * |
1956 | * @throws LogicException |
1957 | */ |
1958 | protected function assertWritableRepo() { |
1959 | } |
1960 | |
1961 | /** |
1962 | * Return information about the repository. |
1963 | * |
1964 | * @return array |
1965 | * @since 1.22 |
1966 | */ |
1967 | public function getInfo() { |
1968 | $ret = [ |
1969 | 'name' => $this->getName(), |
1970 | 'displayname' => $this->getDisplayName(), |
1971 | 'rootUrl' => $this->getZoneUrl( 'public' ), |
1972 | 'local' => $this->isLocal(), |
1973 | ]; |
1974 | |
1975 | $optionalSettings = [ |
1976 | 'url', |
1977 | 'thumbUrl', |
1978 | 'initialCapital', |
1979 | 'descBaseUrl', |
1980 | 'scriptDirUrl', |
1981 | 'articleUrl', |
1982 | 'fetchDescription', |
1983 | 'descriptionCacheExpiry', |
1984 | ]; |
1985 | foreach ( $optionalSettings as $k ) { |
1986 | if ( isset( $this->$k ) ) { |
1987 | $ret[$k] = $this->$k; |
1988 | } |
1989 | } |
1990 | if ( isset( $this->favicon ) ) { |
1991 | // Expand any local path to full URL to improve API usability (T77093). |
1992 | $ret['favicon'] = MediaWikiServices::getInstance()->getUrlUtils() |
1993 | ->expand( $this->favicon ); |
1994 | } |
1995 | |
1996 | return $ret; |
1997 | } |
1998 | |
1999 | /** |
2000 | * Returns whether or not storage is SHA-1 based |
2001 | * @return bool |
2002 | */ |
2003 | public function hasSha1Storage() { |
2004 | return $this->hasSha1Storage; |
2005 | } |
2006 | |
2007 | /** |
2008 | * Returns whether or not repo supports having originals SHA-1s in the thumb URLs |
2009 | * @return bool |
2010 | */ |
2011 | public function supportsSha1URLs() { |
2012 | return $this->supportsSha1URLs; |
2013 | } |
2014 | } |