6 * Bump this number when serialized cache records may be incompatible.
8 define( 'MW_FILE_VERSION', 4 );
11 * Class to represent a local file in the wiki's own database
13 * Provides methods to retrieve paths (physical, logical, URL),
14 * to generate image thumbnails or for uploading.
16 * @addtogroup FileRepo
18 class LocalFile
extends File
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)
28 $bits, # --- returned by getimagesize (loadFromXxx)
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)
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
42 function newFromTitle( $title, $repo ) {
43 return new self( $title, $repo );
46 function newFromRow( $row, $repo ) {
47 $title = Title
::makeTitle( NS_IMAGE
, $row->img_name
);
48 $file = new self( $title, $repo );
49 $file->loadFromRow( $row );
53 function __construct( $title, $repo ) {
54 if( !is_object( $title ) ) {
55 throw new MWException( __CLASS__
.' constructor given bogus title.' );
57 parent
::__construct( $title, $repo );
59 $this->historyLine
= 0;
60 $this->dataLoaded
= false;
64 * Get the memcached key
66 function getCacheKey() {
67 $hashedName = md5($this->getName());
68 return wfMemcKey( 'file', $hashedName );
72 * Try to load file metadata from memcached. Returns true on success.
74 function loadFromCache() {
76 wfProfileIn( __METHOD__
);
77 $this->dataLoaded
= false;
78 $key = $this->getCacheKey();
82 $cachedValues = $wgMemc->get( $key );
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;
96 if ( $this->dataLoaded
) {
97 wfIncrStats( 'image_cache_hit' );
99 wfIncrStats( 'image_cache_miss' );
102 wfProfileOut( __METHOD__
);
103 return $this->dataLoaded
;
107 * Save the file metadata to memcached
109 function saveToCache() {
112 $key = $this->getCacheKey();
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;
125 $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
129 * Load metadata from the file itself
131 function loadFromFile() {
132 wfProfileIn( __METHOD__
);
133 $path = $this->getPath();
134 $this->fileExists
= file_exists( $path );
137 if ( $this->fileExists
) {
138 $magic=& MimeMagic
::singleton();
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
);
146 $this->size
= filesize( $path );
148 # Height, width and metadata
150 $gis = $handler->getImageSize( $this, $path );
151 $this->metadata
= $handler->getMetadata( $this, $path );
154 $this->metadata
= '';
157 wfDebug(__METHOD__
.": $path loaded, {$this->size} bytes, {$this->mime}.\n");
160 $this->media_type
= MEDIATYPE_UNKNOWN
;
161 $this->metadata
= '';
162 wfDebug(__METHOD__
.": $path NOT FOUND!\n");
166 $this->width
= $gis[0];
167 $this->height
= $gis[1];
173 #NOTE: $gis[2] contains a code for the image type. This is no longer used.
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;
180 if ( isset( $gis['bits'] ) ) $this->bits
= $gis['bits'];
181 else $this->bits
= 0;
183 wfProfileOut( __METHOD__
);
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 == '' ) {
193 if ( !isset( $results[$prefix] ) ) {
194 $prefixedFields = array();
195 foreach ( $fields as $field ) {
196 $prefixedFields[] = $prefix . $field;
198 $results[$prefix] = $prefixedFields;
200 return $results[$prefix];
204 * Load file metadata from the DB
206 function loadFromDB() {
207 wfProfileIn( __METHOD__
);
209 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
210 $this->dataLoaded
= true;
212 $dbr = $this->repo
->getSlaveDB();
214 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
215 array( 'img_name' => $this->getName() ), __METHOD__
);
217 $this->loadFromRow( $row );
219 $this->fileExists
= false;
222 wfProfileOut( __METHOD__
);
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.
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' );
237 foreach ( $array as $name => $value ) {
238 $deprefixedName = substr( $name, $prefixLength );
239 $decoded[substr( $name, $prefixLength )] = $value;
241 $decoded['timestamp'] = wfTimestamp( TS_MW
, $decoded['timestamp'] );
242 if ( empty( $decoded['major_mime'] ) ) {
243 $decoded['mime'] = "unknown/unknown";
245 if (!$decoded['minor_mime']) {
246 $decoded['minor_mime'] = "unknown";
248 $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
254 * Load file metadata from a DB result row
256 function loadFromRow( $row, $prefix = 'img_' ) {
257 $array = $this->decodeRow( $row, $prefix );
258 foreach ( $array as $name => $value ) {
259 $this->$name = $value;
261 $this->fileExists
= true;
262 // Check for rows from a previous schema, quietly upgrade them
263 $this->maybeUpgradeRow();
267 * Load file metadata from cache or DB, unless already loaded
270 if ( !$this->dataLoaded
) {
271 if ( !$this->loadFromCache() ) {
273 $this->saveToCache();
275 $this->dataLoaded
= true;
280 * Upgrade a row if it needs it
282 function maybeUpgradeRow() {
283 if ( wfReadOnly() ) {
286 if ( is_null($this->media_type
) ||
$this->mime
== 'image/svg' ) {
288 $this->upgraded
= true;
290 $handler = $this->getHandler();
291 if ( $handler && !$handler->isMetadataValid( $this, $this->metadata
) ) {
293 $this->upgraded
= true;
298 function getUpgraded() {
299 return $this->upgraded
;
303 * Fix assorted version-related problems with the image row by reloading it from the file
305 function upgradeRow() {
306 wfProfileIn( __METHOD__
);
308 $this->loadFromFile();
310 $dbw = $this->repo
->getMasterDB();
311 list( $major, $minor ) = self
::splitMime( $this->mime
);
313 wfDebug(__METHOD__
.': upgrading '.$this->getName()." to the current schema\n");
315 $dbw->update( 'image',
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() ),
327 $this->saveToCache();
328 wfProfileOut( __METHOD__
);
331 /** splitMime inherited */
332 /** getName inherited */
333 /** getTitle inherited */
334 /** getURL inherited */
335 /** getViewURL inherited */
336 /** getPath inherited */
339 * Return the width of the image
341 * Returns false on error
344 function getWidth( $page = 1 ) {
346 if ( $this->isMultipage() ) {
347 $dim = $this->getHandler()->getPageDimensions( $this, $page );
349 return $dim['width'];
359 * Return the height of the image
361 * Returns false on error
364 function getHeight( $page = 1 ) {
366 if ( $this->isMultipage() ) {
367 $dim = $this->getHandler()->getPageDimensions( $this, $page );
369 return $dim['height'];
374 return $this->height
;
379 * Get handler-specific metadata
381 function getMetadata() {
383 return $this->metadata
;
387 * Return the size of the image file, in bytes
396 * Returns the mime type of the file.
398 function getMimeType() {
404 * Return the type of the media in the file.
405 * Use the value returned by this function with the MEDIATYPE_xxx constants.
407 function getMediaType() {
409 return $this->media_type
;
412 /** canRender inherited */
413 /** mustRender inherited */
414 /** allowInlineDisplay inherited */
415 /** isSafeFile inherited */
416 /** isTrustedFile inherited */
419 * Returns true if the file file exists on disk.
420 * @return boolean Whether file file exist on disk.
425 return $this->fileExists
;
428 /** getTransformScript inherited */
429 /** getUnscaledThumb inherited */
430 /** thumbName inherited */
431 /** createThumb inherited */
432 /** getThumbnail inherited */
433 /** transform inherited */
436 * Fix thumbnail files from 1.4 or before, with extreme prejudice
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 );
452 // Doesn't exist anymore
455 if ( is_file( $thumbDir ) ) {
456 // File where directory should be
458 // Doesn't exist anymore
463 /** getHandler inherited */
464 /** iconThumb inherited */
465 /** getLastError inherited */
468 * Get all thumbnail names previously generated for this file
470 function getThumbnails() {
471 if ( $this->isHashed() ) {
474 $dir = $this->getThumbPath();
476 if ( is_dir( $dir ) ) {
477 $handle = opendir( $dir );
480 while ( false !== ( $file = readdir($handle) ) ) {
481 if ( $file{0} != '.' ) {
496 * Refresh metadata in memcached, but don't touch thumbnails or squid
498 function purgeMetadataCache() {
500 $this->loadFromFile();
501 $this->saveToCache();
505 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
507 function purgeCache( $archiveFiles = array() ) {
510 // Refresh metadata cache
511 $this->purgeMetadataCache();
514 $files = $this->getThumbnails();
515 $dir = $this->getThumbPath();
517 foreach ( $files as $file ) {
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 );
524 @unlink
( "$dir/$file" );
530 $urls[] = $this->getURL();
531 foreach ( $archiveFiles as $file ) {
532 $urls[] = $this->getArchiveUrl( $file );
534 wfPurgeSquidServers( $urls );
538 /** purgeDescription inherited */
539 /** purgeEverything inherited */
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
551 function nextHistoryLine() {
552 $dbr = $this->repo
->getSlaveDB();
554 if ( $this->historyLine
== 0 ) {// called for the first time, return line from cur
555 $this->historyRes
= $dbr->select( 'image',
559 'img_user','img_user_text',
563 "'' AS oi_archive_name"
565 array( 'img_name' => $this->title
->getDBkey() ),
568 if ( 0 == $dbr->numRows( $this->historyRes
) ) {
571 } else if ( $this->historyLine
== 1 ) {
572 $this->historyRes
= $dbr->select( 'oldimage',
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',
583 array( 'oi_name' => $this->title
->getDBkey() ),
585 array( 'ORDER BY' => 'oi_timestamp DESC' )
588 $this->historyLine ++
;
590 return $dbr->fetchObject( $this->historyRes
);
594 * Reset the history pointer to the first element of the history
597 function resetHistory() {
598 $this->historyLine
= 0;
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 */
614 * Record a file upload in the upload log and the image table
616 function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
617 $watch = false, $timestamp = false )
619 global $wgUser, $wgUseCopyrightUpload;
621 $dbw = $this->repo
->getMasterDB();
623 // Delete thumbnails and refresh the metadata cache
626 // Fail now if the file isn't there
627 if ( !$this->fileExists
) {
628 wfDebug( __METHOD__
.": File ".$this->getPath()." went missing!\n" );
632 if ( $wgUseCopyrightUpload ) {
633 if ( $license != '' ) {
634 $licensetxt = '== ' . wfMsgForContent( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
636 $textdesc = '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n" .
637 '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
639 '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
641 if ( $license != '' ) {
642 $filedesc = $desc == '' ?
'' : '== ' . wfMsg ( 'filedesc' ) . " ==\n" . $desc . "\n";
643 $textdesc = $filedesc .
644 '== ' . wfMsgForContent ( 'license' ) . " ==\n" . '{{' . $license . '}}' . "\n";
650 if ( $timestamp === false ) {
651 $timestamp = $dbw->timestamp();
655 if (strpos($this->mime
,'/')!==false) {
656 list($major,$minor)= explode('/',$this->mime
,2);
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',
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
,
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',
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__
704 # Update the current image row
705 $dbw->update( 'image',
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()
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__
);
730 $descTitle = $this->getTitle();
731 $article = new Article( $descTitle );
733 $watch = $watch ||
$wgUser->isWatched( $descTitle );
734 $suppressRC = true; // There's already a log entry, so don't double the RC load
736 if( $descTitle->exists() ) {
737 // TODO: insert a null revision into the page history for this update.
739 $wgUser->addWatch( $descTitle );
742 # Invalidate the cache for the description page
743 $descTitle->invalidateCache();
744 $descTitle->purgeSquid();
746 // New file; create the description page.
747 $article->insertNewArticle( $textdesc, $desc, $minor, $watch, $suppressRC );
750 # Hooks, hooks, the magic of hooks...
751 wfRunHooks( 'FileUpload', array( $this ) );
754 $log = new LogPage( 'upload' );
755 $log->addEntry( 'upload', $descTitle, $desc );
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();
761 # Invalidate cache for all pages using this file
762 $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
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.
774 * The archive name should be passed through to recordUpload for database
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
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.
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 ) ) {
792 } elseif ( $status == 'new' ) {
799 /** getLinksTo inherited */
800 /** getExifData inherited */
801 /** isLocal inherited */
802 /** wasDeleted inherited */
805 * Delete all versions of the file.
807 * Moves the files into an archive directory (or deletes them)
808 * and removes the database rows.
810 * Cache purging is done; logging is caller's responsibility.
813 * @return true on success, false on some kind of failure
815 function delete( $reason, $suppress=false ) {
816 $transaction = new FSTransaction();
817 $urlArr = array( $this->getURL() );
819 if( !FileStore
::lock() ) {
820 wfDebug( __METHOD__
.": failed to acquire file store lock, aborting\n" );
825 $dbw = $this->repo
->getMasterDB();
828 // Delete old versions
829 $result = $dbw->select( 'oldimage',
830 array( 'oi_archive_name' ),
831 array( 'oi_name' => $this->getName() ) );
833 while( $row = $dbw->fetchObject( $result ) ) {
834 $oldName = $row->oi_archive_name
;
836 $transaction->add( $this->prepareDeleteOld( $oldName, $reason, $suppress ) );
838 // We'll need to purge this URL from caches...
839 $urlArr[] = $this->getArchiveUrl( $oldName );
841 $dbw->freeResult( $result );
843 // And the current version...
844 $transaction->add( $this->prepareDeleteCurrent( $reason, $suppress ) );
846 $dbw->immediateCommit();
847 } catch( MWException
$e ) {
848 wfDebug( __METHOD__
.": db error, rolling back file transactions\n" );
849 $transaction->rollback();
854 wfDebug( __METHOD__
.": deleted db items, applying file transactions\n" );
855 $transaction->commit();
860 $site_stats = $dbw->tableName( 'site_stats' );
861 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__
);
863 $this->purgeEverything( $urlArr );
870 * Delete an old version of the file.
872 * Moves the file into an archive directory (or deletes it)
873 * and removes the database row.
875 * Cache purging is done; logging is caller's responsibility.
878 * @throws MWException or FSException on database or filestore failure
879 * @return true on success, false on some kind of failure
881 function deleteOld( $archiveName, $reason, $suppress=false ) {
882 $transaction = new FSTransaction();
885 if( !FileStore
::lock() ) {
886 wfDebug( __METHOD__
.": failed to acquire file store lock, aborting\n" );
890 $transaction = new FSTransaction();
892 $dbw = $this->repo
->getMasterDB();
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();
903 wfDebug( __METHOD__
.": deleted db items, applying file transaction\n" );
904 $transaction->commit();
907 $this->purgeDescription();
913 $this->getArchiveUrl( $archiveName ),
915 wfPurgeSquidServers( $urlArr );
921 * Delete the current version of a file.
922 * May throw a database error.
923 * @return true on success, false on failure
925 private function prepareDeleteCurrent( $reason, $suppress=false ) {
926 return $this->prepareDeleteVersion(
927 $this->getFullPath(),
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() ),
951 * Delete a given older version of a file.
952 * May throw a database error.
953 * @return true on success, false on failure
955 private function prepareDeleteOld( $archiveName, $reason, $suppress=false ) {
956 $oldpath = $this->getArchivePath() .
957 DIRECTORY_SEPARATOR
. $archiveName;
958 return $this->prepareDeleteVersion(
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' ),
978 'oi_name' => $this->getName(),
979 'oi_archive_name' => $archiveName ),
985 * Do the dirty work of backing up an image row and its file
986 * (if $wgSaveDeletedFiles is on) and removing the originals.
988 * Must be run while the file store is locked and a database
989 * transaction is open to avoid race conditions.
991 * @return FSTransaction
993 private function prepareDeleteVersion( $path, $reason, $table, $fieldMap, $where, $suppress=false, $fname ) {
994 global $wgUser, $wgSaveDeletedFiles;
996 // Dupe the file into the file store
997 if( file_exists( $path ) ) {
998 if( $wgSaveDeletedFiles ) {
1001 $store = FileStore
::get( $group );
1002 $key = FileStore
::calculateKey( $path, $this->getExtension() );
1003 $transaction = $store->insert( $key, $path,
1004 FileStore
::DELETE_ORIGINAL
);
1008 $transaction = FileStore
::deleteFile( $path );
1011 wfDebug( __METHOD__
." deleting already-missing '$path'; moving on to database\n" );
1014 $transaction = new FSTransaction(); // empty
1017 if( $transaction === false ) {
1019 wfDebug( __METHOD__
.": import to file store failed, aborting\n" );
1020 throw new MWException( "Could not archive and delete file $path" );
1024 // Bitfields to further supress the file content
1025 // Note that currently, live files are stored elsewhere
1026 // and cannot be partially deleted
1029 $bitfield |
= self
::DELETED_FILE
;
1030 $bitfield |
= self
::DELETED_COMMENT
;
1031 $bitfield |
= self
::DELETED_USER
;
1032 $bitfield |
= self
::DELETED_RESTRICTED
;
1035 $dbw = $this->repo
->getMasterDB();
1036 $storageMap = array(
1037 'fa_storage_group' => $dbw->addQuotes( $group ),
1038 'fa_storage_key' => $dbw->addQuotes( $key ),
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 );
1047 if( $wgSaveDeletedFiles ) {
1048 $dbw->insertSelect( 'filearchive', $table, $allFields, $where, $fname );
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();
1059 return $transaction;
1063 * Restore all or specified deleted revisions to the given file.
1064 * Permissions and logging are left to the caller.
1066 * May throw database exceptions on error.
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
1073 function restore( $versions=array(), $Unsuppress=false ) {
1076 if( !FileStore
::lock() ) {
1077 wfDebug( __METHOD__
." could not acquire filestore lock\n" );
1081 $transaction = new FSTransaction();
1083 $dbw = $this->repo
->getMasterDB();
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() ),
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() );
1097 $conditions['fa_id'] = $versions;
1100 $result = $dbw->select( 'filearchive', '*',
1103 array( 'ORDER BY' => 'fa_timestamp DESC' ) );
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" );
1110 FileStore
::unlock();
1114 if( $dbw->numRows( $result ) == 0 ) {
1116 wfDebug( __METHOD__
.": nothing to do\n" );
1118 FileStore
::unlock();
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
1131 $store = FileStore
::get( $row->fa_storage_group
);
1133 wfDebug( __METHOD__
.": skipping row with no file.\n" );
1137 $restoredImage = new self( $row->fa_name
, $this->repo
);
1139 if( $revisions == 1 && !$exists ) {
1140 $destPath = $restoredImage->getFullPath();
1141 $destDir = dirname( $destPath );
1142 if ( !is_dir( $destDir ) ) {
1143 wfMkdirParents( $destDir );
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
);
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 );
1157 $metadata = $handler->getMetadata( false, $tempFile );
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
;
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
);
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>
1190 wfTimestamp( TS_MW
, $row->fa_deleted_timestamp
) .
1191 '!' . $row->fa_name
;
1193 $restoredImage = new self( $row->fa_name
, $this->repo
);
1194 $destDir = $restoredImage->getArchivePath();
1195 if ( !is_dir( $destDir ) ) {
1196 wfMkdirParents( $destDir );
1198 $destPath = $destDir . DIRECTORY_SEPARATOR
. $archiveName;
1200 $table = 'oldimage';
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
);
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
),
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',
1226 'fa_storage_group' => $row->fa_storage_group
,
1227 'fa_storage_key' => $row->fa_storage_key
),
1229 if( $useCount == 0 ) {
1230 wfDebug( __METHOD__
.": nothing else using {$row->fa_storage_key}, will deleting after\n" );
1231 $flags = FileStore
::DELETE_ORIGINAL
;
1236 $transaction->add( $store->export( $row->fa_storage_key
,
1237 $destPath, $flags ) );
1240 $dbw->immediateCommit();
1241 } catch( MWException
$e ) {
1242 wfDebug( __METHOD__
." caught error, aborting\n" );
1243 $transaction->rollback();
1247 $transaction->commit();
1248 FileStore
::unlock();
1250 if( $revisions > 0 ) {
1252 wfDebug( __METHOD__
." restored $revisions items, creating a new current\n" );
1254 // Update site_stats
1255 $site_stats = $dbw->tableName( 'site_stats' );
1256 $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__
);
1258 $this->purgeEverything();
1260 wfDebug( __METHOD__
." restored $revisions as archived versions\n" );
1261 $this->purgeDescription();
1268 /** isMultipage inherited */
1269 /** pageCount inherited */
1270 /** scaleHeight inherited */
1271 /** getImageSize inherited */
1274 * Get the URL of the file description page.
1276 function getDescriptionUrl() {
1277 return $this->title
->getLocalUrl();
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.
1285 function getDescriptionText() {
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
);
1295 function getTimestamp() {
1297 return $this->timestamp
;
1299 } // LocalFile class
1302 * Backwards compatibility class
1304 class Image
extends LocalFile
{
1305 function __construct( $title ) {
1306 $repo = FileRepoGroup
::singleton()->getLocalRepo();
1307 parent
::__construct( $title, $repo );
1311 * Wrapper for wfFindFile(), for backwards-compatibility only
1312 * Do not use in core code.
1314 function newFromTitle( $title, $time = false ) {
1315 $img = wfFindFile( $title, $time );
1317 $img = wfLocalFile( $title );
1324 * Aliases for backwards compatibility with 1.6
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
);