Merged filerepo-work branch:
[lhc/web/wiklou.git] / includes / filerepo / LocalFile.php
1 <?php
2 /**
3 */
4
5 /**
6 * Bump this number when serialized cache records may be incompatible.
7 */
8 define( 'MW_FILE_VERSION', 4 );
9
10 /**
11 * Class to represent a local file in the wiki's own database
12 *
13 * Provides methods to retrieve paths (physical, logical, URL),
14 * to generate image thumbnails or for uploading.
15 *
16 * @addtogroup FileRepo
17 */
18 class LocalFile extends File
19 {
20 /**#@+
21 * @private
22 */
23 var $fileExists, # does the file file exist on disk? (loadFromXxx)
24 $historyLine, # Number of line to return by nextHistoryLine() (constructor)
25 $historyRes, # result of the query for the file's history (nextHistoryLine)
26 $width, # \
27 $height, # |
28 $bits, # --- returned by getimagesize (loadFromXxx)
29 $attr, # /
30 $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
31 $mime, # MIME type, determined by MimeMagic::guessMimeType
32 $major_mime, # Major mime type
33 $minor_mine, # Minor mime type
34 $size, # Size in bytes (loadFromXxx)
35 $metadata, # Metadata
36 $timestamp, # Upload timestamp
37 $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
38 $upgraded; # Whether the row was upgraded on load
39
40 /**#@-*/
41
42 function newFromTitle( $title, $repo ) {
43 return new self( $title, $repo );
44 }
45
46 function newFromRow( $row, $repo ) {
47 $title = Title::makeTitle( NS_IMAGE, $row->img_name );
48 $file = new self( $title, $repo );
49 $file->loadFromRow( $row );
50 return $file;
51 }
52
53 function __construct( $title, $repo ) {
54 if( !is_object( $title ) ) {
55 throw new MWException( __CLASS__.' constructor given bogus title.' );
56 }
57 parent::__construct( $title, $repo );
58 $this->metadata = '';
59 $this->historyLine = 0;
60 $this->dataLoaded = false;
61 }
62
63 /**
64 * Get the memcached key
65 */
66 function getCacheKey() {
67 $hashedName = md5($this->getName());
68 return wfMemcKey( 'file', $hashedName );
69 }
70
71 /**
72 * Try to load file metadata from memcached. Returns true on success.
73 */
74 function loadFromCache() {
75 global $wgMemc;
76 wfProfileIn( __METHOD__ );
77 $this->dataLoaded = false;
78 $key = $this->getCacheKey();
79 if ( !$key ) {
80 return false;
81 }
82 $cachedValues = $wgMemc->get( $key );
83
84 // Check if the key existed and belongs to this version of MediaWiki
85 if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
86 wfDebug( "Pulling file metadata from cache key $key\n" );
87 $this->fileExists = $cachedValues['fileExists'];
88 if ( $this->fileExists ) {
89 unset( $cachedValues['version'] );
90 unset( $cachedValues['fileExists'] );
91 foreach ( $cachedValues as $name => $value ) {
92 $this->$name = $value;
93 }
94 }
95 }
96 if ( $this->dataLoaded ) {
97 wfIncrStats( 'image_cache_hit' );
98 } else {
99 wfIncrStats( 'image_cache_miss' );
100 }
101
102 wfProfileOut( __METHOD__ );
103 return $this->dataLoaded;
104 }
105
106 /**
107 * Save the file metadata to memcached
108 */
109 function saveToCache() {
110 global $wgMemc;
111 $this->load();
112 $key = $this->getCacheKey();
113 if ( !$key ) {
114 return;
115 }
116 $fields = $this->getCacheFields( '' );
117 $cache = array( 'version' => MW_FILE_VERSION );
118 $cache['fileExists'] = $this->fileExists;
119 if ( $this->fileExists ) {
120 foreach ( $fields as $field ) {
121 $cache[$field] = $this->$field;
122 }
123 }
124
125 $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
126 }
127
128 /**
129 * Load metadata from the file itself
130 */
131 function loadFromFile() {
132 wfProfileIn( __METHOD__ );
133 $path = $this->getPath();
134 $this->fileExists = file_exists( $path );
135 $gis = array();
136
137 if ( $this->fileExists ) {
138 $magic=& MimeMagic::singleton();
139
140 $this->mime = $magic->guessMimeType($path,true);
141 list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
142 $this->media_type = $magic->getMediaType($path,$this->mime);
143 $handler = MediaHandler::getHandler( $this->mime );
144
145 # Get size in bytes
146 $this->size = filesize( $path );
147
148 # Height, width and metadata
149 if ( $handler ) {
150 $gis = $handler->getImageSize( $this, $path );
151 $this->metadata = $handler->getMetadata( $this, $path );
152 } else {
153 $gis = false;
154 $this->metadata = '';
155 }
156
157 wfDebug(__METHOD__.": $path loaded, {$this->size} bytes, {$this->mime}.\n");
158 } else {
159 $this->mime = NULL;
160 $this->media_type = MEDIATYPE_UNKNOWN;
161 $this->metadata = '';
162 wfDebug(__METHOD__.": $path NOT FOUND!\n");
163 }
164
165 if( $gis ) {
166 $this->width = $gis[0];
167 $this->height = $gis[1];
168 } else {
169 $this->width = 0;
170 $this->height = 0;
171 }
172
173 #NOTE: $gis[2] contains a code for the image type. This is no longer used.
174
175 #NOTE: we have to set this flag early to avoid load() to be called
176 # be some of the functions below. This may lead to recursion or other bad things!
177 # as ther's only one thread of execution, this should be safe anyway.
178 $this->dataLoaded = true;
179
180 if ( isset( $gis['bits'] ) ) $this->bits = $gis['bits'];
181 else $this->bits = 0;
182
183 wfProfileOut( __METHOD__ );
184 }
185
186 function getCacheFields( $prefix = 'img_' ) {
187 static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
188 'major_mime', 'minor_mime', 'metadata', 'timestamp' );
189 static $results = array();
190 if ( $prefix == '' ) {
191 return $fields;
192 }
193 if ( !isset( $results[$prefix] ) ) {
194 $prefixedFields = array();
195 foreach ( $fields as $field ) {
196 $prefixedFields[] = $prefix . $field;
197 }
198 $results[$prefix] = $prefixedFields;
199 }
200 return $results[$prefix];
201 }
202
203 /**
204 * Load file metadata from the DB
205 */
206 function loadFromDB() {
207 wfProfileIn( __METHOD__ );
208
209 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
210 $this->dataLoaded = true;
211
212 $dbr = $this->repo->getSlaveDB();
213
214 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
215 array( 'img_name' => $this->getName() ), __METHOD__ );
216 if ( $row ) {
217 $this->loadFromRow( $row );
218 } else {
219 $this->fileExists = false;
220 }
221
222 wfProfileOut( __METHOD__ );
223 }
224
225 /**
226 * Decode a row from the database (either object or array) to an array
227 * with timestamps and MIME types decoded, and the field prefix removed.
228 */
229 function decodeRow( $row, $prefix = 'img_' ) {
230 $array = (array)$row;
231 $prefixLength = strlen( $prefix );
232 // Sanity check prefix once
233 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
234 throw new MWException( __METHOD__. ': incorrect $prefix parameter' );
235 }
236 $decoded = array();
237 foreach ( $array as $name => $value ) {
238 $deprefixedName = substr( $name, $prefixLength );
239 $decoded[substr( $name, $prefixLength )] = $value;
240 }
241 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
242 if ( empty( $decoded['major_mime'] ) ) {
243 $decoded['mime'] = "unknown/unknown";
244 } else {
245 if (!$decoded['minor_mime']) {
246 $decoded['minor_mime'] = "unknown";
247 }
248 $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
249 }
250 return $decoded;
251 }
252
253 /*
254 * Load file metadata from a DB result row
255 */
256 function loadFromRow( $row, $prefix = 'img_' ) {
257 $array = $this->decodeRow( $row, $prefix );
258 foreach ( $array as $name => $value ) {
259 $this->$name = $value;
260 }
261 $this->fileExists = true;
262 // Check for rows from a previous schema, quietly upgrade them
263 $this->maybeUpgradeRow();
264 }
265
266 /**
267 * Load file metadata from cache or DB, unless already loaded
268 */
269 function load() {
270 if ( !$this->dataLoaded ) {
271 if ( !$this->loadFromCache() ) {
272 $this->loadFromDB();
273 $this->saveToCache();
274 }
275 $this->dataLoaded = true;
276 }
277 }
278
279 /**
280 * Upgrade a row if it needs it
281 */
282 function maybeUpgradeRow() {
283 if ( wfReadOnly() ) {
284 return;
285 }
286 if ( is_null($this->media_type) || $this->mime == 'image/svg' ) {
287 $this->upgradeRow();
288 $this->upgraded = true;
289 } else {
290 $handler = $this->getHandler();
291 if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
292 $this->upgradeRow();
293 $this->upgraded = true;
294 }
295 }
296 }
297
298 function getUpgraded() {
299 return $this->upgraded;
300 }
301
302 /**
303 * Fix assorted version-related problems with the image row by reloading it from the file
304 */
305 function upgradeRow() {
306 wfProfileIn( __METHOD__ );
307
308 $this->loadFromFile();
309
310 $dbw = $this->repo->getMasterDB();
311 list( $major, $minor ) = self::splitMime( $this->mime );
312
313 wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
314
315 $dbw->update( 'image',
316 array(
317 'img_width' => $this->width,
318 'img_height' => $this->height,
319 'img_bits' => $this->bits,
320 'img_media_type' => $this->media_type,
321 'img_major_mime' => $major,
322 'img_minor_mime' => $minor,
323 'img_metadata' => $this->metadata,
324 ), array( 'img_name' => $this->getName() ),
325 __METHOD__
326 );
327 $this->saveToCache();
328 wfProfileOut( __METHOD__ );
329 }
330
331 /** splitMime inherited */
332 /** getName inherited */
333 /** getTitle inherited */
334 /** getURL inherited */
335 /** getViewURL inherited */
336 /** getPath inherited */
337
338 /**
339 * Return the width of the image
340 *
341 * Returns false on error
342 * @public
343 */
344 function getWidth( $page = 1 ) {
345 $this->load();
346 if ( $this->isMultipage() ) {
347 $dim = $this->getHandler()->getPageDimensions( $this, $page );
348 if ( $dim ) {
349 return $dim['width'];
350 } else {
351 return false;
352 }
353 } else {
354 return $this->width;
355 }
356 }
357
358 /**
359 * Return the height of the image
360 *
361 * Returns false on error
362 * @public
363 */
364 function getHeight( $page = 1 ) {
365 $this->load();
366 if ( $this->isMultipage() ) {
367 $dim = $this->getHandler()->getPageDimensions( $this, $page );
368 if ( $dim ) {
369 return $dim['height'];
370 } else {
371 return false;
372 }
373 } else {
374 return $this->height;
375 }
376 }
377
378 /**
379 * Get handler-specific metadata
380 */
381 function getMetadata() {
382 $this->load();
383 return $this->metadata;
384 }
385
386 /**
387 * Return the size of the image file, in bytes
388 * @public
389 */
390 function getSize() {
391 $this->load();
392 return $this->size;
393 }
394
395 /**
396 * Returns the mime type of the file.
397 */
398 function getMimeType() {
399 $this->load();
400 return $this->mime;
401 }
402
403 /**
404 * Return the type of the media in the file.
405 * Use the value returned by this function with the MEDIATYPE_xxx constants.
406 */
407 function getMediaType() {
408 $this->load();
409 return $this->media_type;
410 }
411
412 /** canRender inherited */
413 /** mustRender inherited */
414 /** allowInlineDisplay inherited */
415 /** isSafeFile inherited */
416 /** isTrustedFile inherited */
417
418 /**
419 * Returns true if the file file exists on disk.
420 * @return boolean Whether file file exist on disk.
421 * @public
422 */
423 function exists() {
424 $this->load();
425 return $this->fileExists;
426 }
427
428 /** getTransformScript inherited */
429 /** getUnscaledThumb inherited */
430 /** thumbName inherited */
431 /** createThumb inherited */
432 /** getThumbnail inherited */
433 /** transform inherited */
434
435 /**
436 * Fix thumbnail files from 1.4 or before, with extreme prejudice
437 */
438 function migrateThumbFile( $thumbName ) {
439 $thumbDir = $this->getThumbPath();
440 $thumbPath = "$thumbDir/$thumbName";
441 if ( is_dir( $thumbPath ) ) {
442 // Directory where file should be
443 // This happened occasionally due to broken migration code in 1.5
444 // Rename to broken-*
445 for ( $i = 0; $i < 100 ; $i++ ) {
446 $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName";
447 if ( !file_exists( $broken ) ) {
448 rename( $thumbPath, $broken );
449 break;
450 }
451 }
452 // Doesn't exist anymore
453 clearstatcache();
454 }
455 if ( is_file( $thumbDir ) ) {
456 // File where directory should be
457 unlink( $thumbDir );
458 // Doesn't exist anymore
459 clearstatcache();
460 }
461 }
462
463 /** getHandler inherited */
464 /** iconThumb inherited */
465 /** getLastError inherited */
466
467 /**
468 * Get all thumbnail names previously generated for this file
469 */
470 function getThumbnails() {
471 if ( $this->isHashed() ) {
472 $this->load();
473 $files = array();
474 $dir = $this->getThumbPath();
475
476 if ( is_dir( $dir ) ) {
477 $handle = opendir( $dir );
478
479 if ( $handle ) {
480 while ( false !== ( $file = readdir($handle) ) ) {
481 if ( $file{0} != '.' ) {
482 $files[] = $file;
483 }
484 }
485 closedir( $handle );
486 }
487 }
488 } else {
489 $files = array();
490 }
491
492 return $files;
493 }
494
495 /**
496 * Refresh metadata in memcached, but don't touch thumbnails or squid
497 */
498 function purgeMetadataCache() {
499 clearstatcache();
500 $this->loadFromFile();
501 $this->saveToCache();
502 }
503
504 /**
505 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
506 */
507 function purgeCache( $archiveFiles = array() ) {
508 global $wgUseSquid;
509
510 // Refresh metadata cache
511 $this->purgeMetadataCache();
512
513 // Delete thumbnails
514 $files = $this->getThumbnails();
515 $dir = $this->getThumbPath();
516 $urls = array();
517 foreach ( $files as $file ) {
518 $m = array();
519 # Check that the base file name is part of the thumb name
520 # This is a basic sanity check to avoid erasing unrelated directories
521 if ( strpos( $file, $this->getName() ) !== false ) {
522 $url = $this->getThumbUrl( $file );
523 $urls[] = $url;
524 @unlink( "$dir/$file" );
525 }
526 }
527
528 // Purge the squid
529 if ( $wgUseSquid ) {
530 $urls[] = $this->getURL();
531 foreach ( $archiveFiles as $file ) {
532 $urls[] = $this->getArchiveUrl( $file );
533 }
534 wfPurgeSquidServers( $urls );
535 }
536 }
537
538 /** purgeDescription inherited */
539 /** purgeEverything inherited */
540
541 /**
542 * Return the history of this file, line by line.
543 * starts with current version, then old versions.
544 * uses $this->historyLine to check which line to return:
545 * 0 return line for current version
546 * 1 query for old versions, return first one
547 * 2, ... return next old version from above query
548 *
549 * @public
550 */
551 function nextHistoryLine() {
552 $dbr = $this->repo->getSlaveDB();
553
554 if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
555 $this->historyRes = $dbr->select( 'image',
556 array(
557 'img_size',
558 'img_description',
559 'img_user','img_user_text',
560 'img_timestamp',
561 'img_width',
562 'img_height',
563 "'' AS oi_archive_name"
564 ),
565 array( 'img_name' => $this->title->getDBkey() ),
566 __METHOD__
567 );
568 if ( 0 == $dbr->numRows( $this->historyRes ) ) {
569 return FALSE;
570 }
571 } else if ( $this->historyLine == 1 ) {
572 $this->historyRes = $dbr->select( 'oldimage',
573 array(
574 'oi_size AS img_size',
575 'oi_description AS img_description',
576 'oi_user AS img_user',
577 'oi_user_text AS img_user_text',
578 'oi_timestamp AS img_timestamp',
579 'oi_width as img_width',
580 'oi_height as img_height',
581 'oi_archive_name'
582 ),
583 array( 'oi_name' => $this->title->getDBkey() ),
584 __METHOD__,
585 array( 'ORDER BY' => 'oi_timestamp DESC' )
586 );
587 }
588 $this->historyLine ++;
589
590 return $dbr->fetchObject( $this->historyRes );
591 }
592
593 /**
594 * Reset the history pointer to the first element of the history
595 * @public
596 */
597 function resetHistory() {
598 $this->historyLine = 0;
599 }
600
601 /** getFullPath inherited */
602 /** getHashPath inherited */
603 /** getRel inherited */
604 /** getUrlRel inherited */
605 /** getArchivePath inherited */
606 /** getThumbPath inherited */
607 /** getArchiveUrl inherited */
608 /** getThumbUrl inherited */
609 /** getArchiveVirtualUrl inherited */
610 /** getThumbVirtualUrl inherited */
611 /** isHashed inherited */
612
613 /**
614 * Record a file upload in the upload log and the image table
615 */
616 function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
617 $watch = false, $timestamp = false )
618 {
619 global $wgUser, $wgUseCopyrightUpload;
620
621 $dbw = $this->repo->getMasterDB();
622
623 // Delete thumbnails and refresh the metadata cache
624 $this->purgeCache();
625
626 // Fail now if the file isn't there
627 if ( !$this->fileExists ) {
628 wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" );
629 return false;
630 }
631
632 if ( $wgUseCopyrightUpload ) {
633 if ( $license != '' ) {
634 $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
635 }
636 $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" .
637 '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
638 "$licensetxt" .
639 '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
640 } else {
641 if ( $license != '' ) {
642 $filedesc = $desc == '' ? '' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n";
643 $textdesc = $filedesc .
644 '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
645 } else {
646 $textdesc = $desc;
647 }
648 }
649
650 if ( $timestamp === false ) {
651 $timestamp = $dbw->timestamp();
652 }
653
654 #split mime type
655 if (strpos($this->mime,'/')!==false) {
656 list($major,$minor)= explode('/',$this->mime,2);
657 }
658 else {
659 $major= $this->mime;
660 $minor= "unknown";
661 }
662
663 # Test to see if the row exists using INSERT IGNORE
664 # This avoids race conditions by locking the row until the commit, and also
665 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
666 $dbw->insert( 'image',
667 array(
668 'img_name' => $this->getName(),
669 'img_size'=> $this->size,
670 'img_width' => intval( $this->width ),
671 'img_height' => intval( $this->height ),
672 'img_bits' => $this->bits,
673 'img_media_type' => $this->media_type,
674 'img_major_mime' => $major,
675 'img_minor_mime' => $minor,
676 'img_timestamp' => $timestamp,
677 'img_description' => $desc,
678 'img_user' => $wgUser->getID(),
679 'img_user_text' => $wgUser->getName(),
680 'img_metadata' => $this->metadata,
681 ),
682 __METHOD__,
683 'IGNORE'
684 );
685
686 if( $dbw->affectedRows() == 0 ) {
687 # Collision, this is an update of a file
688 # Insert previous contents into oldimage
689 $dbw->insertSelect( 'oldimage', 'image',
690 array(
691 'oi_name' => 'img_name',
692 'oi_archive_name' => $dbw->addQuotes( $oldver ),
693 'oi_size' => 'img_size',
694 'oi_width' => 'img_width',
695 'oi_height' => 'img_height',
696 'oi_bits' => 'img_bits',
697 'oi_timestamp' => 'img_timestamp',
698 'oi_description' => 'img_description',
699 'oi_user' => 'img_user',
700 'oi_user_text' => 'img_user_text',
701 ), array( 'img_name' => $this->getName() ), __METHOD__
702 );
703
704 # Update the current image row
705 $dbw->update( 'image',
706 array( /* SET */
707 'img_size' => $this->size,
708 'img_width' => intval( $this->width ),
709 'img_height' => intval( $this->height ),
710 'img_bits' => $this->bits,
711 'img_media_type' => $this->media_type,
712 'img_major_mime' => $major,
713 'img_minor_mime' => $minor,
714 'img_timestamp' => $timestamp,
715 'img_description' => $desc,
716 'img_user' => $wgUser->getID(),
717 'img_user_text' => $wgUser->getName(),
718 'img_metadata' => $this->metadata,
719 ), array( /* WHERE */
720 'img_name' => $this->getName()
721 ), __METHOD__
722 );
723 } else {
724 # This is a new file
725 # Update the image count
726 $site_stats = $dbw->tableName( 'site_stats' );
727 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
728 }
729
730 $descTitle = $this->getTitle();
731 $article = new Article( $descTitle );
732 $minor = false;
733 $watch = $watch || $wgUser->isWatched( $descTitle );
734 $suppressRC = true; // There's already a log entry, so don't double the RC load
735
736 if( $descTitle->exists() ) {
737 // TODO: insert a null revision into the page history for this update.
738 if( $watch ) {
739 $wgUser->addWatch( $descTitle );
740 }
741
742 # Invalidate the cache for the description page
743 $descTitle->invalidateCache();
744 $descTitle->purgeSquid();
745 } else {
746 // New file; create the description page.
747 $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC );
748 }
749
750 # Hooks, hooks, the magic of hooks...
751 wfRunHooks( 'FileUpload', array( $this ) );
752
753 # Add the log entry
754 $log = new LogPage( 'upload' );
755 $log->addEntry( 'upload', $descTitle, $desc );
756
757 # Commit the transaction now, in case something goes wrong later
758 # The most important thing is that files don't get lost, especially archives
759 $dbw->immediateCommit();
760
761 # Invalidate cache for all pages using this file
762 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
763 $update->doUpdate();
764
765 return true;
766 }
767
768 /**
769 * Move or copy a file to its public location. If a file exists at the
770 * destination, move it to an archive. Returns the archive name on success
771 * or an empty string if it was a new file, and a wikitext-formatted
772 * WikiError object on failure.
773 *
774 * The archive name should be passed through to recordUpload for database
775 * registration.
776 *
777 * @param string $sourcePath Local filesystem path to the source image
778 * @param integer $flags A bitwise combination of:
779 * File::DELETE_SOURCE Delete the source file, i.e. move
780 * rather than copy
781 * @return The archive name on success or an empty string if it was a new
782 * file, and a wikitext-formatted WikiError object on failure.
783 */
784 function publish( $srcPath, $flags = 0 ) {
785 $dstPath = $this->getFullPath();
786 $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName();
787 $archivePath = $this->getArchivePath( $archiveName );
788 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
789 $status = $this->repo->publish( $srcPath, $dstPath, $archivePath, $flags );
790 if ( WikiError::isError( $status ) ) {
791 return $status;
792 } elseif ( $status == 'new' ) {
793 return '';
794 } else {
795 return $archiveName;
796 }
797 }
798
799 /** getLinksTo inherited */
800 /** getExifData inherited */
801 /** isLocal inherited */
802 /** wasDeleted inherited */
803
804 /**
805 * Delete all versions of the file.
806 *
807 * Moves the files into an archive directory (or deletes them)
808 * and removes the database rows.
809 *
810 * Cache purging is done; logging is caller's responsibility.
811 *
812 * @param $reason
813 * @return true on success, false on some kind of failure
814 */
815 function delete( $reason, $suppress=false ) {
816 $transaction = new FSTransaction();
817 $urlArr = array( $this->getURL() );
818
819 if( !FileStore::lock() ) {
820 wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
821 return false;
822 }
823
824 try {
825 $dbw = $this->repo->getMasterDB();
826 $dbw->begin();
827
828 // Delete old versions
829 $result = $dbw->select( 'oldimage',
830 array( 'oi_archive_name' ),
831 array( 'oi_name' => $this->getName() ) );
832
833 while( $row = $dbw->fetchObject( $result ) ) {
834 $oldName = $row->oi_archive_name;
835
836 $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) );
837
838 // We'll need to purge this URL from caches...
839 $urlArr[] = $this->getArchiveUrl( $oldName );
840 }
841 $dbw->freeResult( $result );
842
843 // And the current version...
844 $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) );
845
846 $dbw->immediateCommit();
847 } catch( MWException $e ) {
848 wfDebug( __METHOD__.": db error, rolling back file transactions\n" );
849 $transaction->rollback();
850 FileStore::unlock();
851 throw $e;
852 }
853
854 wfDebug( __METHOD__.": deleted db items, applying file transactions\n" );
855 $transaction->commit();
856 FileStore::unlock();
857
858
859 // Update site_stats
860 $site_stats = $dbw->tableName( 'site_stats' );
861 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
862
863 $this->purgeEverything( $urlArr );
864
865 return true;
866 }
867
868
869 /**
870 * Delete an old version of the file.
871 *
872 * Moves the file into an archive directory (or deletes it)
873 * and removes the database row.
874 *
875 * Cache purging is done; logging is caller's responsibility.
876 *
877 * @param $reason
878 * @throws MWException or FSException on database or filestore failure
879 * @return true on success, false on some kind of failure
880 */
881 function deleteOld( $archiveName, $reason, $suppress=false ) {
882 $transaction = new FSTransaction();
883 $urlArr = array();
884
885 if( !FileStore::lock() ) {
886 wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" );
887 return false;
888 }
889
890 $transaction = new FSTransaction();
891 try {
892 $dbw = $this->repo->getMasterDB();
893 $dbw->begin();
894 $transaction->add( $this->prepareDeleteOld( $archiveName, $reason, $suppress ) );
895 $dbw->immediateCommit();
896 } catch( MWException $e ) {
897 wfDebug( __METHOD__.": db error, rolling back file transaction\n" );
898 $transaction->rollback();
899 FileStore::unlock();
900 throw $e;
901 }
902
903 wfDebug( __METHOD__.": deleted db items, applying file transaction\n" );
904 $transaction->commit();
905 FileStore::unlock();
906
907 $this->purgeDescription();
908
909 // Squid purging
910 global $wgUseSquid;
911 if ( $wgUseSquid ) {
912 $urlArr = array(
913 $this->getArchiveUrl( $archiveName ),
914 );
915 wfPurgeSquidServers( $urlArr );
916 }
917 return true;
918 }
919
920 /**
921 * Delete the current version of a file.
922 * May throw a database error.
923 * @return true on success, false on failure
924 */
925 private function prepareDeleteCurrent( $reason, $suppress=false ) {
926 return $this->prepareDeleteVersion(
927 $this->getFullPath(),
928 $reason,
929 'image',
930 array(
931 'fa_name' => 'img_name',
932 'fa_archive_name' => 'NULL',
933 'fa_size' => 'img_size',
934 'fa_width' => 'img_width',
935 'fa_height' => 'img_height',
936 'fa_metadata' => 'img_metadata',
937 'fa_bits' => 'img_bits',
938 'fa_media_type' => 'img_media_type',
939 'fa_major_mime' => 'img_major_mime',
940 'fa_minor_mime' => 'img_minor_mime',
941 'fa_description' => 'img_description',
942 'fa_user' => 'img_user',
943 'fa_user_text' => 'img_user_text',
944 'fa_timestamp' => 'img_timestamp' ),
945 array( 'img_name' => $this->getName() ),
946 $suppress,
947 __METHOD__ );
948 }
949
950 /**
951 * Delete a given older version of a file.
952 * May throw a database error.
953 * @return true on success, false on failure
954 */
955 private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) {
956 $oldpath = $this->getArchivePath() .
957 DIRECTORY_SEPARATOR . $archiveName;
958 return $this->prepareDeleteVersion(
959 $oldpath,
960 $reason,
961 'oldimage',
962 array(
963 'fa_name' => 'oi_name',
964 'fa_archive_name' => 'oi_archive_name',
965 'fa_size' => 'oi_size',
966 'fa_width' => 'oi_width',
967 'fa_height' => 'oi_height',
968 'fa_metadata' => 'NULL',
969 'fa_bits' => 'oi_bits',
970 'fa_media_type' => 'NULL',
971 'fa_major_mime' => 'NULL',
972 'fa_minor_mime' => 'NULL',
973 'fa_description' => 'oi_description',
974 'fa_user' => 'oi_user',
975 'fa_user_text' => 'oi_user_text',
976 'fa_timestamp' => 'oi_timestamp' ),
977 array(
978 'oi_name' => $this->getName(),
979 'oi_archive_name' => $archiveName ),
980 $suppress,
981 __METHOD__ );
982 }
983
984 /**
985 * Do the dirty work of backing up an image row and its file
986 * (if $wgSaveDeletedFiles is on) and removing the originals.
987 *
988 * Must be run while the file store is locked and a database
989 * transaction is open to avoid race conditions.
990 *
991 * @return FSTransaction
992 */
993 private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) {
994 global $wgUser, $wgSaveDeletedFiles;
995
996 // Dupe the file into the file store
997 if( file_exists( $path ) ) {
998 if( $wgSaveDeletedFiles ) {
999 $group = 'deleted';
1000
1001 $store = FileStore::get( $group );
1002 $key = FileStore::calculateKey( $path, $this->getExtension() );
1003 $transaction = $store->insert( $key, $path,
1004 FileStore::DELETE_ORIGINAL );
1005 } else {
1006 $group = null;
1007 $key = null;
1008 $transaction = FileStore::deleteFile( $path );
1009 }
1010 } else {
1011 wfDebug( __METHOD__." deleting already-missing '$path'; moving on to database\n" );
1012 $group = null;
1013 $key = null;
1014 $transaction = new FSTransaction(); // empty
1015 }
1016
1017 if( $transaction === false ) {
1018 // Fail to restore?
1019 wfDebug( __METHOD__.": import to file store failed, aborting\n" );
1020 throw new MWException( "Could not archive and delete file $path" );
1021 return false;
1022 }
1023
1024 // Bitfields to further supress the file content
1025 // Note that currently, live files are stored elsewhere
1026 // and cannot be partially deleted
1027 $bitfield = 0;
1028 if ( $suppress ) {
1029 $bitfield |= self::DELETED_FILE;
1030 $bitfield |= self::DELETED_COMMENT;
1031 $bitfield |= self::DELETED_USER;
1032 $bitfield |= self::DELETED_RESTRICTED;
1033 }
1034
1035 $dbw = $this->repo->getMasterDB();
1036 $storageMap = array(
1037 'fa_storage_group' => $dbw->addQuotes( $group ),
1038 'fa_storage_key' => $dbw->addQuotes( $key ),
1039
1040 'fa_deleted_user' => $dbw->addQuotes( $wgUser->getId() ),
1041 'fa_deleted_timestamp' => $dbw->timestamp(),
1042 'fa_deleted_reason' => $dbw->addQuotes( $reason ),
1043 'fa_deleted' => $bitfield);
1044 $allFields = array_merge( $storageMap, $fieldMap );
1045
1046 try {
1047 if( $wgSaveDeletedFiles ) {
1048 $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
1049 }
1050 $dbw->delete( $table, $where, $fname );
1051 } catch( DBQueryError $e ) {
1052 // Something went horribly wrong!
1053 // Leave the file as it was...
1054 wfDebug( __METHOD__.": database error, rolling back file transaction\n" );
1055 $transaction->rollback();
1056 throw $e;
1057 }
1058
1059 return $transaction;
1060 }
1061
1062 /**
1063 * Restore all or specified deleted revisions to the given file.
1064 * Permissions and logging are left to the caller.
1065 *
1066 * May throw database exceptions on error.
1067 *
1068 * @param $versions set of record ids of deleted items to restore,
1069 * or empty to restore all revisions.
1070 * @return the number of file revisions restored if successful,
1071 * or false on failure
1072 */
1073 function restore( $versions=array(), $Unsuppress=false ) {
1074 global $wgUser;
1075
1076 if( !FileStore::lock() ) {
1077 wfDebug( __METHOD__." could not acquire filestore lock\n" );
1078 return false;
1079 }
1080
1081 $transaction = new FSTransaction();
1082 try {
1083 $dbw = $this->repo->getMasterDB();
1084 $dbw->begin();
1085
1086 // Re-confirm whether this file presently exists;
1087 // if no we'll need to create an file record for the
1088 // first item we restore.
1089 $exists = $dbw->selectField( 'image', '1',
1090 array( 'img_name' => $this->getName() ),
1091 __METHOD__ );
1092
1093 // Fetch all or selected archived revisions for the file,
1094 // sorted from the most recent to the oldest.
1095 $conditions = array( 'fa_name' => $this->getName() );
1096 if( $versions ) {
1097 $conditions['fa_id'] = $versions;
1098 }
1099
1100 $result = $dbw->select( 'filearchive', '*',
1101 $conditions,
1102 __METHOD__,
1103 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
1104
1105 if( $dbw->numRows( $result ) < count( $versions ) ) {
1106 // There's some kind of conflict or confusion;
1107 // we can't restore everything we were asked to.
1108 wfDebug( __METHOD__.": couldn't find requested items\n" );
1109 $dbw->rollback();
1110 FileStore::unlock();
1111 return false;
1112 }
1113
1114 if( $dbw->numRows( $result ) == 0 ) {
1115 // Nothing to do.
1116 wfDebug( __METHOD__.": nothing to do\n" );
1117 $dbw->rollback();
1118 FileStore::unlock();
1119 return true;
1120 }
1121
1122 $revisions = 0;
1123 while( $row = $dbw->fetchObject( $result ) ) {
1124 if ( $Unsuppress ) {
1125 // Currently, fa_deleted flags fall off upon restore, lets be careful about this
1126 } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) {
1127 // Skip restoring file revisions that the user cannot restore
1128 continue;
1129 }
1130 $revisions++;
1131 $store = FileStore::get( $row->fa_storage_group );
1132 if( !$store ) {
1133 wfDebug( __METHOD__.": skipping row with no file.\n" );
1134 continue;
1135 }
1136
1137 $restoredImage = new self( $row->fa_name, $this->repo );
1138
1139 if( $revisions == 1 && !$exists ) {
1140 $destPath = $restoredImage->getFullPath();
1141 $destDir = dirname( $destPath );
1142 if ( !is_dir( $destDir ) ) {
1143 wfMkdirParents( $destDir );
1144 }
1145
1146 // We may have to fill in data if this was originally
1147 // an archived file revision.
1148 if( is_null( $row->fa_metadata ) ) {
1149 $tempFile = $store->filePath( $row->fa_storage_key );
1150
1151 $magic = MimeMagic::singleton();
1152 $mime = $magic->guessMimeType( $tempFile, true );
1153 $media_type = $magic->getMediaType( $tempFile, $mime );
1154 list( $major_mime, $minor_mime ) = self::splitMime( $mime );
1155 $handler = MediaHandler::getHandler( $mime );
1156 if ( $handler ) {
1157 $metadata = $handler->getMetadata( false, $tempFile );
1158 } else {
1159 $metadata = '';
1160 }
1161 } else {
1162 $metadata = $row->fa_metadata;
1163 $major_mime = $row->fa_major_mime;
1164 $minor_mime = $row->fa_minor_mime;
1165 $media_type = $row->fa_media_type;
1166 }
1167
1168 $table = 'image';
1169 $fields = array(
1170 'img_name' => $row->fa_name,
1171 'img_size' => $row->fa_size,
1172 'img_width' => $row->fa_width,
1173 'img_height' => $row->fa_height,
1174 'img_metadata' => $metadata,
1175 'img_bits' => $row->fa_bits,
1176 'img_media_type' => $media_type,
1177 'img_major_mime' => $major_mime,
1178 'img_minor_mime' => $minor_mime,
1179 'img_description' => $row->fa_description,
1180 'img_user' => $row->fa_user,
1181 'img_user_text' => $row->fa_user_text,
1182 'img_timestamp' => $row->fa_timestamp );
1183 } else {
1184 $archiveName = $row->fa_archive_name;
1185 if( $archiveName == '' ) {
1186 // This was originally a current version; we
1187 // have to devise a new archive name for it.
1188 // Format is <timestamp of archiving>!<name>
1189 $archiveName =
1190 wfTimestamp( TS_MW, $row->fa_deleted_timestamp ) .
1191 '!' . $row->fa_name;
1192 }
1193 $restoredImage = new self( $row->fa_name, $this->repo );
1194 $destDir = $restoredImage->getArchivePath();
1195 if ( !is_dir( $destDir ) ) {
1196 wfMkdirParents( $destDir );
1197 }
1198 $destPath = $destDir . DIRECTORY_SEPARATOR . $archiveName;
1199
1200 $table = 'oldimage';
1201 $fields = array(
1202 'oi_name' => $row->fa_name,
1203 'oi_archive_name' => $archiveName,
1204 'oi_size' => $row->fa_size,
1205 'oi_width' => $row->fa_width,
1206 'oi_height' => $row->fa_height,
1207 'oi_bits' => $row->fa_bits,
1208 'oi_description' => $row->fa_description,
1209 'oi_user' => $row->fa_user,
1210 'oi_user_text' => $row->fa_user_text,
1211 'oi_timestamp' => $row->fa_timestamp );
1212 }
1213
1214 $dbw->insert( $table, $fields, __METHOD__ );
1215 // @todo this delete is not totally safe, potentially
1216 $dbw->delete( 'filearchive',
1217 array( 'fa_id' => $row->fa_id ),
1218 __METHOD__ );
1219
1220 // Check if any other stored revisions use this file;
1221 // if so, we shouldn't remove the file from the deletion
1222 // archives so they will still work.
1223 $useCount = $dbw->selectField( 'filearchive',
1224 'COUNT(*)',
1225 array(
1226 'fa_storage_group' => $row->fa_storage_group,
1227 'fa_storage_key' => $row->fa_storage_key ),
1228 __METHOD__ );
1229 if( $useCount == 0 ) {
1230 wfDebug( __METHOD__.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
1231 $flags = FileStore::DELETE_ORIGINAL;
1232 } else {
1233 $flags = 0;
1234 }
1235
1236 $transaction->add( $store->export( $row->fa_storage_key,
1237 $destPath, $flags ) );
1238 }
1239
1240 $dbw->immediateCommit();
1241 } catch( MWException $e ) {
1242 wfDebug( __METHOD__." caught error, aborting\n" );
1243 $transaction->rollback();
1244 throw $e;
1245 }
1246
1247 $transaction->commit();
1248 FileStore::unlock();
1249
1250 if( $revisions > 0 ) {
1251 if( !$exists ) {
1252 wfDebug( __METHOD__." restored $revisions items, creating a new current\n" );
1253
1254 // Update site_stats
1255 $site_stats = $dbw->tableName( 'site_stats' );
1256 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1257
1258 $this->purgeEverything();
1259 } else {
1260 wfDebug( __METHOD__." restored $revisions as archived versions\n" );
1261 $this->purgeDescription();
1262 }
1263 }
1264
1265 return $revisions;
1266 }
1267
1268 /** isMultipage inherited */
1269 /** pageCount inherited */
1270 /** scaleHeight inherited */
1271 /** getImageSize inherited */
1272
1273 /**
1274 * Get the URL of the file description page.
1275 */
1276 function getDescriptionUrl() {
1277 return $this->title->getLocalUrl();
1278 }
1279
1280 /**
1281 * Get the HTML text of the description page
1282 * This is not used by ImagePage for local files, since (among other things)
1283 * it skips the parser cache.
1284 */
1285 function getDescriptionText() {
1286 global $wgParser;
1287 $revision = Revision::newFromTitle( $this->title );
1288 if ( !$revision ) return false;
1289 $text = $revision->getText();
1290 if ( !$text ) return false;
1291 $html = $wgParser->parse( $text, new ParserOptions );
1292 return $html;
1293 }
1294
1295 function getTimestamp() {
1296 $this->load();
1297 return $this->timestamp;
1298 }
1299 } // LocalFile class
1300
1301 /**
1302 * Backwards compatibility class
1303 */
1304 class Image extends LocalFile {
1305 function __construct( $title ) {
1306 $repo = FileRepoGroup::singleton()->getLocalRepo();
1307 parent::__construct( $title, $repo );
1308 }
1309
1310 /**
1311 * Wrapper for wfFindFile(), for backwards-compatibility only
1312 * Do not use in core code.
1313 */
1314 function newFromTitle( $title, $time = false ) {
1315 $img = wfFindFile( $title, $time );
1316 if ( !$img ) {
1317 $img = wfLocalFile( $title );
1318 }
1319 return $img;
1320 }
1321 }
1322
1323 /**
1324 * Aliases for backwards compatibility with 1.6
1325 */
1326 define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
1327 define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
1328 define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
1329 define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );
1330
1331 ?>