Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 154 |
|
0.00% |
0 / 25 |
CRAP | |
0.00% |
0 / 1 |
OldLocalFile | |
0.00% |
0 / 154 |
|
0.00% |
0 / 25 |
1980 | |
0.00% |
0 / 1 |
newFromTitle | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
newFromArchiveName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newFromRow | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromKey | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getQueryInfo | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
loadFromRow | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getArchiveName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isOld | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isVisible | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
loadFromDB | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
loadExtraFromDB | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
buildQueryBuilderForLoad | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getCacheFields | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getRel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUrlRel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
upgradeRow | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
6 | |||
reserializeMetadata | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isDeleted | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getVisibility | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
userCan | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
uploadOld | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
recordOldUpload | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
6 | |||
exists | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\FileRepo\File\FileSelectQueryBuilder; |
22 | use MediaWiki\MediaWikiServices; |
23 | use MediaWiki\Permissions\Authority; |
24 | use MediaWiki\Revision\RevisionRecord; |
25 | use MediaWiki\Status\Status; |
26 | use MediaWiki\Title\Title; |
27 | use MediaWiki\User\UserIdentity; |
28 | use Wikimedia\Rdbms\IReadableDatabase; |
29 | use Wikimedia\Rdbms\SelectQueryBuilder; |
30 | |
31 | /** |
32 | * Old file in the oldimage table. |
33 | * |
34 | * @stable to extend |
35 | * @ingroup FileAbstraction |
36 | */ |
37 | class OldLocalFile extends LocalFile { |
38 | /** @var string|int Timestamp */ |
39 | protected $requestedTime; |
40 | |
41 | /** @var string Archive name */ |
42 | protected $archive_name; |
43 | |
44 | public const CACHE_VERSION = 1; |
45 | |
46 | /** |
47 | * @stable to override |
48 | * @param Title $title |
49 | * @param LocalRepo $repo |
50 | * @param string|int|null $time |
51 | * @return static |
52 | */ |
53 | public static function newFromTitle( $title, $repo, $time = null ) { |
54 | # The null default value is only here to avoid an E_STRICT |
55 | if ( $time === null ) { |
56 | throw new InvalidArgumentException( __METHOD__ . ' got null for $time parameter' ); |
57 | } |
58 | |
59 | return new static( $title, $repo, $time, null ); |
60 | } |
61 | |
62 | /** |
63 | * @stable to override |
64 | * |
65 | * @param Title $title |
66 | * @param LocalRepo $repo |
67 | * @param string $archiveName |
68 | * @return static |
69 | */ |
70 | public static function newFromArchiveName( $title, $repo, $archiveName ) { |
71 | return new static( $title, $repo, null, $archiveName ); |
72 | } |
73 | |
74 | /** |
75 | * @stable to override |
76 | * |
77 | * @param stdClass $row |
78 | * @param LocalRepo $repo |
79 | * @return static |
80 | */ |
81 | public static function newFromRow( $row, $repo ) { |
82 | $title = Title::makeTitle( NS_FILE, $row->oi_name ); |
83 | $file = new static( $title, $repo, null, $row->oi_archive_name ); |
84 | $file->loadFromRow( $row, 'oi_' ); |
85 | |
86 | return $file; |
87 | } |
88 | |
89 | /** |
90 | * Create a OldLocalFile from a SHA-1 key |
91 | * Do not call this except from inside a repo class. |
92 | * |
93 | * @stable to override |
94 | * |
95 | * @param string $sha1 Base-36 SHA-1 |
96 | * @param LocalRepo $repo |
97 | * @param string|false $timestamp MW_timestamp (optional) |
98 | * |
99 | * @return static|false |
100 | */ |
101 | public static function newFromKey( $sha1, $repo, $timestamp = false ) { |
102 | $dbr = $repo->getReplicaDB(); |
103 | $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr ); |
104 | |
105 | $queryBuilder->where( [ 'oi_sha1' => $sha1 ] ); |
106 | if ( $timestamp ) { |
107 | $queryBuilder->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $timestamp ) ] ); |
108 | } |
109 | |
110 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
111 | if ( $row ) { |
112 | return static::newFromRow( $row, $repo ); |
113 | } else { |
114 | return false; |
115 | } |
116 | } |
117 | |
118 | /** |
119 | * Return the tables, fields, and join conditions to be selected to create |
120 | * a new oldlocalfile object. |
121 | * |
122 | * Since 1.34, oi_user and oi_user_text have not been present in the |
123 | * database, but they continue to be available in query results as |
124 | * aliases. |
125 | * |
126 | * @since 1.31 |
127 | * @stable to override |
128 | * |
129 | * @deprecated since 1.41 use FileSelectQueryBuilder instead |
130 | * @param string[] $options |
131 | * - omit-lazy: Omit fields that are lazily cached. |
132 | * @return array[] With three keys: |
133 | * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` |
134 | * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` |
135 | * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` |
136 | * @phan-return array{tables:string[],fields:string[],joins:array} |
137 | */ |
138 | public static function getQueryInfo( array $options = [] ) { |
139 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
140 | $queryInfo = FileSelectQueryBuilder::newForOldFile( $dbr, $options )->getQueryInfo(); |
141 | return [ |
142 | 'tables' => $queryInfo['tables'], |
143 | 'fields' => $queryInfo['fields'], |
144 | 'joins' => $queryInfo['join_conds'], |
145 | ]; |
146 | } |
147 | |
148 | /** |
149 | * @stable to call |
150 | * |
151 | * @param Title $title |
152 | * @param LocalRepo $repo |
153 | * @param string|int|null $time Timestamp or null to load by archive name |
154 | * @param string|null $archiveName Archive name or null to load by timestamp |
155 | */ |
156 | public function __construct( $title, $repo, $time, $archiveName ) { |
157 | parent::__construct( $title, $repo ); |
158 | $this->requestedTime = $time; |
159 | $this->archive_name = $archiveName; |
160 | if ( $time === null && $archiveName === null ) { |
161 | throw new LogicException( __METHOD__ . ': must specify at least one of $time or $archiveName' ); |
162 | } |
163 | } |
164 | |
165 | public function loadFromRow( $row, $prefix = 'img_' ) { |
166 | $this->archive_name = $row->{"{$prefix}archive_name"}; |
167 | $this->deleted = $row->{"{$prefix}deleted"}; |
168 | $row = clone $row; |
169 | unset( $row->{"{$prefix}archive_name"} ); |
170 | unset( $row->{"{$prefix}deleted"} ); |
171 | parent::loadFromRow( $row, $prefix ); |
172 | } |
173 | |
174 | /** |
175 | * @stable to override |
176 | * @return bool |
177 | */ |
178 | protected function getCacheKey() { |
179 | return false; |
180 | } |
181 | |
182 | /** |
183 | * @stable to override |
184 | * @return string |
185 | */ |
186 | public function getArchiveName() { |
187 | if ( !isset( $this->archive_name ) ) { |
188 | $this->load(); |
189 | } |
190 | |
191 | return $this->archive_name; |
192 | } |
193 | |
194 | /** |
195 | * @return bool |
196 | */ |
197 | public function isOld() { |
198 | return true; |
199 | } |
200 | |
201 | /** |
202 | * @return bool |
203 | */ |
204 | public function isVisible() { |
205 | return $this->exists() && !$this->isDeleted( File::DELETED_FILE ); |
206 | } |
207 | |
208 | /** |
209 | * @stable to override |
210 | * @param int $flags |
211 | */ |
212 | protected function loadFromDB( $flags = 0 ) { |
213 | $this->dataLoaded = true; |
214 | |
215 | $dbr = ( $flags & IDBAccessObject::READ_LATEST ) |
216 | ? $this->repo->getPrimaryDB() |
217 | : $this->repo->getReplicaDB(); |
218 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr, [] ); |
219 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
220 | if ( $row ) { |
221 | $this->loadFromRow( $row, 'oi_' ); |
222 | } else { |
223 | $this->fileExists = false; |
224 | } |
225 | } |
226 | |
227 | /** |
228 | * Load lazy file metadata from the DB |
229 | * @stable to override |
230 | */ |
231 | protected function loadExtraFromDB() { |
232 | $this->extraDataLoaded = true; |
233 | $dbr = $this->repo->getReplicaDB(); |
234 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr ); |
235 | |
236 | // In theory the file could have just been renamed/deleted...oh well |
237 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
238 | |
239 | if ( !$row ) { // fallback to primary DB |
240 | $dbr = $this->repo->getPrimaryDB(); |
241 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr ); |
242 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
243 | } |
244 | |
245 | if ( $row ) { |
246 | foreach ( $this->unprefixRow( $row, 'oi_' ) as $name => $value ) { |
247 | $this->$name = $value; |
248 | } |
249 | } else { |
250 | throw new RuntimeException( "Could not find data for image '{$this->archive_name}'." ); |
251 | } |
252 | } |
253 | |
254 | private function buildQueryBuilderForLoad( IReadableDatabase $dbr, $options = [ 'omit-nonlazy' ] ) { |
255 | $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr, $options ); |
256 | $queryBuilder->where( [ 'oi_name' => $this->getName() ] ) |
257 | ->orderBy( 'oi_timestamp', SelectQueryBuilder::SORT_DESC ); |
258 | if ( $this->requestedTime === null ) { |
259 | $queryBuilder->andWhere( [ 'oi_archive_name' => $this->archive_name ] ); |
260 | } else { |
261 | $queryBuilder->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $this->requestedTime ) ] ); |
262 | } |
263 | return $queryBuilder; |
264 | } |
265 | |
266 | /** |
267 | * @inheritDoc |
268 | * @stable to override |
269 | */ |
270 | protected function getCacheFields( $prefix = 'img_' ) { |
271 | $fields = parent::getCacheFields( $prefix ); |
272 | $fields[] = $prefix . 'archive_name'; |
273 | $fields[] = $prefix . 'deleted'; |
274 | |
275 | return $fields; |
276 | } |
277 | |
278 | /** |
279 | * @return string |
280 | * @stable to override |
281 | */ |
282 | public function getRel() { |
283 | return $this->getArchiveRel( $this->getArchiveName() ); |
284 | } |
285 | |
286 | /** |
287 | * @return string |
288 | * @stable to override |
289 | */ |
290 | public function getUrlRel() { |
291 | return $this->getArchiveRel( rawurlencode( $this->getArchiveName() ) ); |
292 | } |
293 | |
294 | /** |
295 | * @stable to override |
296 | */ |
297 | public function upgradeRow() { |
298 | $this->loadFromFile(); |
299 | |
300 | # Don't destroy file info of missing files |
301 | if ( !$this->fileExists ) { |
302 | wfDebug( __METHOD__ . ": file does not exist, aborting" ); |
303 | |
304 | return; |
305 | } |
306 | |
307 | $dbw = $this->repo->getPrimaryDB(); |
308 | [ $major, $minor ] = self::splitMime( $this->mime ); |
309 | |
310 | wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . " to the current schema" ); |
311 | $dbw->newUpdateQueryBuilder() |
312 | ->update( 'oldimage' ) |
313 | ->set( [ |
314 | 'oi_size' => $this->size, |
315 | 'oi_width' => $this->width, |
316 | 'oi_height' => $this->height, |
317 | 'oi_bits' => $this->bits, |
318 | 'oi_media_type' => $this->media_type, |
319 | 'oi_major_mime' => $major, |
320 | 'oi_minor_mime' => $minor, |
321 | 'oi_metadata' => $this->getMetadataForDb( $dbw ), |
322 | 'oi_sha1' => $this->sha1, |
323 | ] ) |
324 | ->where( [ |
325 | 'oi_name' => $this->getName(), |
326 | 'oi_archive_name' => $this->archive_name, |
327 | ] ) |
328 | ->caller( __METHOD__ )->execute(); |
329 | } |
330 | |
331 | protected function reserializeMetadata() { |
332 | // TODO: implement this and make it possible to hit it from refreshImageMetadata.php |
333 | // It can be hit from action=purge but that's not very useful if the |
334 | // goal is to reserialize the whole oldimage table. |
335 | } |
336 | |
337 | /** |
338 | * @param int $field One of DELETED_* bitfield constants for file or |
339 | * revision rows |
340 | * @return bool |
341 | */ |
342 | public function isDeleted( $field ) { |
343 | $this->load(); |
344 | |
345 | return ( $this->deleted & $field ) == $field; |
346 | } |
347 | |
348 | /** |
349 | * Returns bitfield value |
350 | * @return int |
351 | */ |
352 | public function getVisibility() { |
353 | $this->load(); |
354 | |
355 | return (int)$this->deleted; |
356 | } |
357 | |
358 | /** |
359 | * Determine if the current user is allowed to view a particular |
360 | * field of this image file, if it's marked as deleted. |
361 | * |
362 | * @param int $field |
363 | * @param Authority $performer User object to check |
364 | * @return bool |
365 | */ |
366 | public function userCan( $field, Authority $performer ) { |
367 | $this->load(); |
368 | |
369 | return RevisionRecord::userCanBitfield( |
370 | $this->deleted, |
371 | $field, |
372 | $performer |
373 | ); |
374 | } |
375 | |
376 | /** |
377 | * Upload a file directly into archive. Generally for Special:Import. |
378 | * |
379 | * @param string $srcPath File system path of the source file |
380 | * @param string $timestamp |
381 | * @param string $comment |
382 | * @param UserIdentity $user |
383 | * @return Status |
384 | */ |
385 | public function uploadOld( $srcPath, $timestamp, $comment, UserIdentity $user ) { |
386 | $archiveName = $this->getArchiveName(); |
387 | $dstRel = $this->getArchiveRel( $archiveName ); |
388 | $status = $this->publishTo( $srcPath, $dstRel ); |
389 | |
390 | if ( $status->isGood() && |
391 | !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) |
392 | ) { |
393 | $status->fatal( 'filenotfound', $srcPath ); |
394 | } |
395 | |
396 | return $status; |
397 | } |
398 | |
399 | /** |
400 | * Record a file upload in the oldimage table, without adding log entries. |
401 | * @stable to override |
402 | * |
403 | * @param string $srcPath File system path to the source file |
404 | * @param string $archiveName The archive name of the file |
405 | * @param string $timestamp |
406 | * @param string $comment Upload comment |
407 | * @param UserIdentity $user User who did this upload |
408 | * @return bool |
409 | */ |
410 | protected function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) { |
411 | $dbw = $this->repo->getPrimaryDB(); |
412 | |
413 | $services = MediaWikiServices::getInstance(); |
414 | $mwProps = new MWFileProps( $services->getMimeAnalyzer() ); |
415 | $props = $mwProps->getPropsFromPath( $srcPath, true ); |
416 | if ( !$props['fileExists'] ) { |
417 | return false; |
418 | } |
419 | $this->setProps( $props ); |
420 | |
421 | $dbw->startAtomic( __METHOD__ ); |
422 | $commentFields = $services->getCommentStore() |
423 | ->insert( $dbw, 'oi_description', $comment ); |
424 | $actorId = $services->getActorNormalization() |
425 | ->acquireActorId( $user, $dbw ); |
426 | $dbw->newInsertQueryBuilder() |
427 | ->insertInto( 'oldimage' ) |
428 | ->row( [ |
429 | 'oi_name' => $this->getName(), |
430 | 'oi_archive_name' => $archiveName, |
431 | 'oi_size' => $props['size'], |
432 | 'oi_width' => intval( $props['width'] ), |
433 | 'oi_height' => intval( $props['height'] ), |
434 | 'oi_bits' => $props['bits'], |
435 | 'oi_actor' => $actorId, |
436 | 'oi_timestamp' => $dbw->timestamp( $timestamp ), |
437 | 'oi_metadata' => $this->getMetadataForDb( $dbw ), |
438 | 'oi_media_type' => $props['media_type'], |
439 | 'oi_major_mime' => $props['major_mime'], |
440 | 'oi_minor_mime' => $props['minor_mime'], |
441 | 'oi_sha1' => $props['sha1'], |
442 | ] + $commentFields ) |
443 | ->caller( __METHOD__ )->execute(); |
444 | $dbw->endAtomic( __METHOD__ ); |
445 | |
446 | return true; |
447 | } |
448 | |
449 | /** |
450 | * If archive name is an empty string, then file does not "exist" |
451 | * |
452 | * This is the case for a couple files on Wikimedia servers where |
453 | * the old version is "lost". |
454 | * @return bool |
455 | */ |
456 | public function exists() { |
457 | $archiveName = $this->getArchiveName(); |
458 | if ( $archiveName === '' || !is_string( $archiveName ) ) { |
459 | return false; |
460 | } |
461 | return parent::exists(); |
462 | } |
463 | } |