diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2014-12-27 15:41:37 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2014-12-31 11:43:28 +0100 |
commit | c1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch) | |
tree | 2b38796e738dd74cb42ecd9bfd151803108386bc /includes/filerepo/file | |
parent | b88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff) |
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/filerepo/file')
-rw-r--r-- | includes/filerepo/file/ArchivedFile.php | 165 | ||||
-rw-r--r-- | includes/filerepo/file/File.php | 827 | ||||
-rw-r--r-- | includes/filerepo/file/ForeignAPIFile.php | 53 | ||||
-rw-r--r-- | includes/filerepo/file/ForeignDBFile.php | 58 | ||||
-rw-r--r-- | includes/filerepo/file/LocalFile.php | 994 | ||||
-rw-r--r-- | includes/filerepo/file/OldLocalFile.php | 125 | ||||
-rw-r--r-- | includes/filerepo/file/UnregisteredLocalFile.php | 63 |
7 files changed, 1482 insertions, 803 deletions
diff --git a/includes/filerepo/file/ArchivedFile.php b/includes/filerepo/file/ArchivedFile.php index 749f11a5..5b0d8e2b 100644 --- a/includes/filerepo/file/ArchivedFile.php +++ b/includes/filerepo/file/ArchivedFile.php @@ -27,40 +27,73 @@ * @ingroup FileAbstraction */ class ArchivedFile { - /**#@+ - * @private - */ - var $id, # filearchive row ID - $name, # image name - $group, # FileStore storage group - $key, # FileStore sha1 key - $size, # file dimensions - $bits, # size in bytes - $width, # width - $height, # height - $metadata, # metadata string - $mime, # mime type - $media_type, # media type - $description, # upload description - $user, # user ID of uploader - $user_text, # user name of uploader - $timestamp, # time of upload - $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) - $deleted, # Bitfield akin to rev_deleted - $sha1, # sha1 hash of file content - $pageCount, - $archive_name; + /** @var int Filearchive row ID */ + private $id; - /** - * @var MediaHandler - */ - var $handler; - /** - * @var Title + /** @var string File name */ + private $name; + + /** @var string FileStore storage group */ + private $group; + + /** @var string FileStore SHA-1 key */ + private $key; + + /** @var int File size in bytes */ + private $size; + + /** @var int Size in bytes */ + private $bits; + + /** @var int Width */ + private $width; + + /** @var int Height */ + private $height; + + /** @var string Metadata string */ + private $metadata; + + /** @var string MIME type */ + private $mime; + + /** @var string Media type */ + private $media_type; + + /** @var string Upload description */ + private $description; + + /** @var int User ID of uploader */ + private $user; + + /** @var string User name of uploader */ + private $user_text; + + /** @var string Time of upload */ + private $timestamp; + + /** @var bool Whether or not all this has been loaded from the database (loadFromXxx) */ + private $dataLoaded; + + /** @var int Bitfield akin to rev_deleted */ + private $deleted; + + /** @var string SHA-1 hash of file content */ + private $sha1; + + /** @var string Number of pages of a multipage document, or false for + * documents which aren't multipage documents */ - var $title; # image title + private $pageCount; + + /** @var string Original base filename */ + private $archive_name; - /**#@-*/ + /** @var MediaHandler */ + protected $handler; + + /** @var Title */ + protected $title; # image title /** * @throws MWException @@ -162,13 +195,13 @@ class ArchivedFile { /** * Loads a file object from the filearchive table * - * @param $row - * + * @param stdClass $row * @return ArchivedFile */ public static function newFromRow( $row ) { $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) ); $file->loadFromRow( $row ); + return $file; } @@ -204,7 +237,7 @@ class ArchivedFile { /** * Load ArchivedFile object fields from a DB row. * - * @param $row Object database row + * @param stdClass $row Object database row * @since 1.21 */ public function loadFromRow( $row ) { @@ -231,6 +264,9 @@ class ArchivedFile { // old row, populate from key $this->sha1 = LocalRepo::getHashFromKey( $this->key ); } + if ( !$this->title ) { + $this->title = Title::makeTitleSafe( NS_FILE, $row->fa_name ); + } } /** @@ -239,6 +275,9 @@ class ArchivedFile { * @return Title */ public function getTitle() { + if ( !$this->title ) { + $this->load(); + } return $this->title; } @@ -248,6 +287,10 @@ class ArchivedFile { * @return string */ public function getName() { + if ( $this->name === false ) { + $this->load(); + } + return $this->name; } @@ -256,6 +299,7 @@ class ArchivedFile { */ public function getID() { $this->load(); + return $this->id; } @@ -264,6 +308,7 @@ class ArchivedFile { */ public function exists() { $this->load(); + return $this->exists; } @@ -273,6 +318,7 @@ class ArchivedFile { */ public function getKey() { $this->load(); + return $this->key; } @@ -298,6 +344,7 @@ class ArchivedFile { */ public function getWidth() { $this->load(); + return $this->width; } @@ -307,6 +354,7 @@ class ArchivedFile { */ public function getHeight() { $this->load(); + return $this->height; } @@ -316,6 +364,7 @@ class ArchivedFile { */ public function getMetadata() { $this->load(); + return $this->metadata; } @@ -325,6 +374,7 @@ class ArchivedFile { */ public function getSize() { $this->load(); + return $this->size; } @@ -334,15 +384,17 @@ class ArchivedFile { */ public function getBits() { $this->load(); + return $this->bits; } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * @return string */ public function getMimeType() { $this->load(); + return $this->mime; } @@ -354,12 +406,14 @@ class ArchivedFile { if ( !isset( $this->handler ) ) { $this->handler = MediaHandler::getHandler( $this->getMimeType() ); } + return $this->handler; } /** * Returns the number of pages of a multipage document, or false for * documents which aren't multipage documents + * @return bool|int */ function pageCount() { if ( !isset( $this->pageCount ) ) { @@ -369,6 +423,7 @@ class ArchivedFile { $this->pageCount = false; } } + return $this->pageCount; } @@ -379,6 +434,7 @@ class ArchivedFile { */ public function getMediaType() { $this->load(); + return $this->media_type; } @@ -389,6 +445,7 @@ class ArchivedFile { */ public function getTimestamp() { $this->load(); + return wfTimestamp( TS_MW, $this->timestamp ); } @@ -400,29 +457,40 @@ class ArchivedFile { */ function getSha1() { $this->load(); + return $this->sha1; } /** - * Return the user ID of the uploader. + * Returns ID or name of user who uploaded the file * - * @return int + * @note Prior to MediaWiki 1.23, this method always + * returned the user id, and was inconsistent with + * the rest of the file classes. + * @param string $type 'text' or 'id' + * @return int|string + * @throws MWException */ - public function getUser() { + public function getUser( $type = 'text' ) { $this->load(); - if ( $this->isDeleted( File::DELETED_USER ) ) { - return 0; - } else { + + if ( $type == 'text' ) { + return $this->user_text; + } elseif ( $type == 'id' ) { return $this->user; } + + throw new MWException( "Unknown type '$type'." ); } /** * Return the user name of the uploader. * + * @deprecated since 1.23 Use getUser( 'text' ) instead. * @return string */ public function getUserText() { + wfDeprecated( __METHOD__, '1.23' ); $this->load(); if ( $this->isDeleted( File::DELETED_USER ) ) { return 0; @@ -452,6 +520,7 @@ class ArchivedFile { */ public function getRawUser() { $this->load(); + return $this->user; } @@ -462,6 +531,7 @@ class ArchivedFile { */ public function getRawUserText() { $this->load(); + return $this->user_text; } @@ -472,6 +542,7 @@ class ArchivedFile { */ public function getRawDescription() { $this->load(); + return $this->description; } @@ -481,29 +552,33 @@ class ArchivedFile { */ public function getVisibility() { $this->load(); + return $this->deleted; } /** * for file or revision rows * - * @param $field Integer: one of DELETED_* bitfield constants + * @param int $field One of DELETED_* bitfield constants * @return bool */ public function isDeleted( $field ) { $this->load(); + return ( $this->deleted & $field ) == $field; } /** * Determine if the current user is allowed to view a particular * field of this FileStore image file, if it's marked as deleted. - * @param $field Integer - * @param $user User object to check, or null to use $wgUser + * @param int $field + * @param null|User $user User object to check, or null to use $wgUser * @return bool */ public function userCan( $field, User $user = null ) { $this->load(); - return Revision::userCanBitfield( $this->deleted, $field, $user ); + + $title = $this->getTitle(); + return Revision::userCanBitfield( $this->deleted, $field, $user, $title ? : null ); } } diff --git a/includes/filerepo/file/File.php b/includes/filerepo/file/File.php index ec5f927b..b574c5e7 100644 --- a/includes/filerepo/file/File.php +++ b/includes/filerepo/file/File.php @@ -91,45 +91,67 @@ abstract class File { * The following member variables are not lazy-initialised */ - /** - * @var FileRepo|bool - */ - var $repo; + /** @var FileRepo|LocalRepo|ForeignAPIRepo|bool */ + public $repo; - /** - * @var Title - */ - var $title; + /** @var Title|string|bool */ + protected $title; - var $lastError, $redirected, $redirectedTitle; + /** @var string Text of last error */ + protected $lastError; - /** - * @var FSFile|bool False if undefined - */ + /** @var string Main part of the title, with underscores (Title::getDBkey) */ + protected $redirected; + + /** @var Title */ + protected $redirectedTitle; + + /** @var FSFile|bool False if undefined */ protected $fsFile; - /** - * @var MediaHandler - */ + /** @var MediaHandler */ protected $handler; - /** - * @var string + /** @var string The URL corresponding to one of the four basic zones */ + protected $url; + + /** @var string File extension */ + protected $extension; + + /** @var string The name of a file from its title object */ + protected $name; + + /** @var string The storage path corresponding to one of the zones */ + protected $path; + + /** @var string Relative path including trailing slash */ + protected $hashPath; + + /** @var string Number of pages of a multipage document, or false for + * documents which aren't multipage documents */ - protected $url, $extension, $name, $path, $hashPath, $pageCount, $transformScript; + protected $pageCount; + /** @var string URL of transformscript (for example thumb.php) */ + protected $transformScript; + + /** @var Title */ protected $redirectTitle; - /** - * @var bool - */ - protected $canRender, $isSafeFile; + /** @var bool Wether the output of transform() for this file is likely to be valid. */ + protected $canRender; - /** - * @var string Required Repository class type + /** @var bool Wether this media file is in a format that is unlikely to + * contain viruses or malicious content */ + protected $isSafeFile; + + /** @var string Required Repository class type */ protected $repoClass = 'FileRepo'; + /** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */ + protected $tmpBucketedThumbCache = array(); + /** * Call this constructor from child classes. * @@ -137,8 +159,8 @@ abstract class File { * may return false or throw exceptions if they are not set. * Most subclasses will want to call assertRepoDefined() here. * - * @param $title Title|string|bool - * @param $repo FileRepo|bool + * @param Title|string|bool $title + * @param FileRepo|bool $repo */ function __construct( $title, $repo ) { if ( $title !== false ) { // subclasses may not use MW titles @@ -152,7 +174,7 @@ abstract class File { * Given a string or Title object return either a * valid Title object with namespace NS_FILE or null * - * @param $title Title|string + * @param Title|string $title * @param string|bool $exception Use 'exception' to throw an error on bad titles * @throws MWException * @return Title|null @@ -174,6 +196,7 @@ abstract class File { if ( !$ret && $exception !== false ) { throw new MWException( "`$title` is not a valid file title." ); } + return $ret; } @@ -183,6 +206,7 @@ abstract class File { return null; } else { $this->$name = call_user_func( $function ); + return $this->$name; } } @@ -214,7 +238,7 @@ abstract class File { /** * Checks if file extensions are compatible * - * @param $old File Old file + * @param File $old Old file * @param string $new New name * * @return bool|null @@ -224,6 +248,7 @@ abstract class File { $n = strrpos( $new, '.' ); $newExt = self::normalizeExtension( $n ? substr( $new, $n + 1 ) : '' ); $mimeMagic = MimeMagic::singleton(); + return $mimeMagic->isMatchingExtension( $newExt, $oldMime ); } @@ -232,7 +257,8 @@ abstract class File { * Called by ImagePage * STUB */ - function upgradeRow() {} + function upgradeRow() { + } /** * Split an internet media type into its two components; if not @@ -252,10 +278,9 @@ abstract class File { /** * Callback for usort() to do file sorts by name * - * @param $a File - * @param $b File - * - * @return Integer: result of name comparison + * @param File $a + * @param File $b + * @return int Result of name comparison */ public static function compare( File $a, File $b ) { return strcmp( $a->getName(), $b->getName() ); @@ -271,6 +296,7 @@ abstract class File { $this->assertRepoDefined(); $this->name = $this->repo->getNameFromTitle( $this->title ); } + return $this->name; } @@ -285,6 +311,7 @@ abstract class File { $this->extension = self::normalizeExtension( $n ? substr( $this->getName(), $n + 1 ) : '' ); } + return $this->extension; } @@ -306,6 +333,7 @@ abstract class File { if ( $this->redirected ) { return $this->getRedirectedTitle(); } + return $this->title; } @@ -320,6 +348,7 @@ abstract class File { $ext = $this->getExtension(); $this->url = $this->repo->getZoneUrl( 'public', $ext ) . '/' . $this->getUrlRel(); } + return $this->url; } @@ -328,7 +357,7 @@ abstract class File { * Upload URL paths _may or may not_ be fully qualified, so * we check. Local paths are assumed to belong on $wgServer. * - * @return String + * @return string */ public function getFullUrl() { return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE ); @@ -351,6 +380,7 @@ abstract class File { } else { wfDebug( __METHOD__ . ': supposed to render ' . $this->getName() . ' (' . $this->getMimeType() . "), but can't!\n" ); + return $this->getURL(); #hm... return NULL? } } else { @@ -376,6 +406,7 @@ abstract class File { $this->assertRepoDefined(); $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel(); } + return $this->path; } @@ -394,6 +425,7 @@ abstract class File { $this->fsFile = false; // null => false; cache negative hits } } + return ( $this->fsFile ) ? $this->fsFile->getPath() : false; @@ -406,9 +438,8 @@ abstract class File { * STUB * Overridden by LocalFile, UnregisteredLocalFile * - * @param $page int - * - * @return number + * @param int $page + * @return int|bool */ public function getWidth( $page = 1 ) { return false; @@ -421,20 +452,62 @@ abstract class File { * STUB * Overridden by LocalFile, UnregisteredLocalFile * - * @param $page int - * - * @return bool|number False on failure + * @param int $page + * @return bool|int False on failure */ public function getHeight( $page = 1 ) { return false; } /** + * Return the smallest bucket from $wgThumbnailBuckets which is at least + * $wgThumbnailMinimumBucketDistance larger than $desiredWidth. The returned bucket, if any, + * will always be bigger than $desiredWidth. + * + * @param int $desiredWidth + * @param int $page + * @return bool|int + */ + public function getThumbnailBucket( $desiredWidth, $page = 1 ) { + global $wgThumbnailBuckets, $wgThumbnailMinimumBucketDistance; + + $imageWidth = $this->getWidth( $page ); + + if ( $imageWidth === false ) { + return false; + } + + if ( $desiredWidth > $imageWidth ) { + return false; + } + + if ( !$wgThumbnailBuckets ) { + return false; + } + + $sortedBuckets = $wgThumbnailBuckets; + + sort( $sortedBuckets ); + + foreach ( $sortedBuckets as $bucket ) { + if ( $bucket > $imageWidth ) { + return false; + } + + if ( $bucket - $wgThumbnailMinimumBucketDistance > $desiredWidth ) { + return $bucket; + } + } + + // Image is bigger than any available bucket + return false; + } + + /** * Returns ID or name of user who uploaded the file * STUB * * @param string $type 'text' or 'id' - * * @return string|int */ public function getUser( $type = 'text' ) { @@ -444,7 +517,7 @@ abstract class File { /** * Get the duration of a media file in seconds * - * @return number + * @return int */ public function getLength() { $handler = $this->getHandler(); @@ -470,11 +543,47 @@ abstract class File { } /** + * Gives a (possibly empty) list of languages to render + * the file in. + * + * If the file doesn't have translations, or if the file + * format does not support that sort of thing, returns + * an empty array. + * + * @return array + * @since 1.23 + */ + public function getAvailableLanguages() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getAvailableLanguages( $this ); + } else { + return array(); + } + } + + /** + * In files that support multiple language, what is the default language + * to use if none specified. + * + * @return string Lang code, or null if filetype doesn't support multiple languages. + * @since 1.23 + */ + public function getDefaultRenderLanguage() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->getDefaultRenderLanguage( $this ); + } else { + return null; + } + } + + /** * Will the thumbnail be animated if one would expect it to be. * * Currently used to add a warning to the image description page * - * @return bool false if the main image is both animated + * @return bool False if the main image is both animated * and the thumbnail is not. In all other cases must return * true. If image is not renderable whatsoever, should * return true. @@ -506,18 +615,35 @@ abstract class File { * Get handler-specific metadata * Overridden by LocalFile, UnregisteredLocalFile * STUB - * @return bool + * @return bool|array */ public function getMetadata() { return false; } /** + * Like getMetadata but returns a handler independent array of common values. + * @see MediaHandler::getCommonMetaArray() + * @return array|bool Array or false if not supported + * @since 1.23 + */ + public function getCommonMetaArray() { + $handler = $this->getHandler(); + + if ( !$handler ) { + return false; + } + + return $handler->getCommonMetaArray( $this ); + } + + /** * get versioned metadata * - * @param $metadata Mixed Array or String of (serialized) metadata - * @param $version integer version number. - * @return Array containing metadata, or what was passed to it on fail (unserializing if not array) + * @param array|string $metadata Array or string of (serialized) metadata + * @param int $version Version number. + * @return array Array containing metadata, or what was passed to it on fail + * (unserializing if not array) */ public function convertMetadataVersion( $metadata, $version ) { $handler = $this->getHandler(); @@ -553,7 +679,7 @@ abstract class File { } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * Overridden by LocalFile, UnregisteredLocalFile * STUB * @@ -588,8 +714,9 @@ abstract class File { */ function canRender() { if ( !isset( $this->canRender ) ) { - $this->canRender = $this->getHandler() && $this->handler->canRender( $this ); + $this->canRender = $this->getHandler() && $this->handler->canRender( $this ) && $this->exists(); } + return $this->canRender; } @@ -639,8 +766,9 @@ abstract class File { */ function isSafeFile() { if ( !isset( $this->isSafeFile ) ) { - $this->isSafeFile = $this->_getIsSafeFile(); + $this->isSafeFile = $this->getIsSafeFileUncached(); } + return $this->isSafeFile; } @@ -658,7 +786,7 @@ abstract class File { * * @return bool */ - protected function _getIsSafeFile() { + protected function getIsSafeFileUncached() { global $wgTrustedMediaFormats; if ( $this->allowInlineDisplay() ) { @@ -713,7 +841,7 @@ abstract class File { * * Overridden by LocalFile to avoid unnecessary stat calls. * - * @return boolean Whether file exists in the repository. + * @return bool Whether file exists in the repository. */ public function exists() { return $this->getPath() && $this->repo->fileExists( $this->path ); @@ -723,7 +851,7 @@ abstract class File { * Returns true if file exists in the repository and can be included in a page. * It would be unsafe to include private images, making public thumbnails inadvertently * - * @return boolean Whether file exists in the repository and is includable. + * @return bool Whether file exists in the repository and is includable. */ public function isVisible() { return $this->exists(); @@ -742,13 +870,14 @@ abstract class File { } } } + return $this->transformScript; } /** * Get a ThumbnailImage which is the same size as the source * - * @param $handlerParams array + * @param array $handlerParams * * @return string */ @@ -760,6 +889,9 @@ abstract class File { return $this->iconThumb(); } $hp['width'] = $width; + // be sure to ignore any height specification as well (bug 62258) + unset( $hp['height'] ); + return $this->transform( $hp ); } @@ -768,14 +900,15 @@ abstract class File { * Use File::THUMB_FULL_NAME to always get a name like "<params>-<source>". * Otherwise, the format may be "<params>-<source>" or "<params>-thumbnail.<ext>". * - * @param array $params handler-specific parameters - * @param $flags integer Bitfield that supports THUMB_* constants + * @param array $params Handler-specific parameters + * @param int $flags Bitfield that supports THUMB_* constants * @return string */ public function thumbName( $params, $flags = 0 ) { $name = ( $this->repo && !( $flags & self::THUMB_FULL_NAME ) ) ? $this->repo->nameForThumb( $this->getName() ) : $this->getName(); + return $this->generateThumbName( $name, $params ); } @@ -784,7 +917,6 @@ abstract class File { * * @param string $name * @param array $params Parameters which will be passed to MediaHandler::makeParamString - * * @return string */ public function generateThumbName( $name, $params ) { @@ -792,12 +924,13 @@ abstract class File { return null; } $extension = $this->getExtension(); - list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( + list( $thumbExt, ) = $this->getHandler()->getThumbType( $extension, $this->getMimeType(), $params ); - $thumbName = $this->handler->makeParamString( $params ) . '-' . $name; + $thumbName = $this->getHandler()->makeParamString( $params ) . '-' . $name; if ( $thumbExt != $extension ) { $thumbName .= ".$thumbExt"; } + return $thumbName; } @@ -813,8 +946,8 @@ abstract class File { * specified, the generated image will be no bigger than width x height, * and will also have correct aspect ratio. * - * @param $width Integer: maximum width of the generated thumbnail - * @param $height Integer: maximum height of the image (optional) + * @param int $width Maximum width of the generated thumbnail + * @param int $height Maximum height of the image (optional) * * @return string */ @@ -824,9 +957,10 @@ abstract class File { $params['height'] = $height; } $thumb = $this->transform( $params ); - if ( is_null( $thumb ) || $thumb->isError() ) { + if ( !$thumb || $thumb->isError() ) { return ''; } + return $thumb->getUrl(); } @@ -835,8 +969,8 @@ abstract class File { * * @param string $thumbPath Thumbnail storage path * @param string $thumbUrl Thumbnail URL - * @param $params Array - * @param $flags integer + * @param array $params + * @param int $flags * @return MediaTransformOutput */ protected function transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ) { @@ -854,13 +988,13 @@ abstract class File { /** * Transform a media file * - * @param array $params an associative array of handler-specific parameters. - * Typical keys are width, height and page. - * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering + * @param array $params An associative array of handler-specific parameters. + * Typical keys are width, height and page. + * @param int $flags A bitfield, may contain self::RENDER_NOW to force rendering * @return MediaTransformOutput|bool False on failure */ function transform( $params, $flags = 0 ) { - global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch; + global $wgThumbnailEpoch; wfProfileIn( __METHOD__ ); do { @@ -895,15 +1029,13 @@ abstract class File { if ( $this->repo ) { // Defer rendering if a 404 handler is set up... if ( $this->repo->canTransformVia404() && !( $flags & self::RENDER_NOW ) ) { - wfDebug( __METHOD__ . " transformation deferred." ); + wfDebug( __METHOD__ . " transformation deferred.\n" ); // XXX: Pass in the storage path even though we are not rendering anything // and the path is supposed to be an FS path. This is due to getScalerType() // getting called on the path and clobbering $thumb->getUrl() if it's false. $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); break; } - // Clean up broken thumbnails as needed - $this->migrateThumbFile( $thumbName ); // Check if an up-to-date thumbnail already exists... wfDebug( __METHOD__ . ": Doing stat for $thumbPath\n" ); if ( !( $flags & self::RENDER_FORCE ) && $this->repo->fileExists( $thumbPath ) ) { @@ -919,94 +1051,274 @@ abstract class File { } elseif ( $flags & self::RENDER_FORCE ) { wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" ); } - } - // If the backend is ready-only, don't keep generating thumbnails - // only to return transformation errors, just return the error now. - if ( $this->repo->getReadOnlyReason() !== false ) { - $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); - break; + // If the backend is ready-only, don't keep generating thumbnails + // only to return transformation errors, just return the error now. + if ( $this->repo->getReadOnlyReason() !== false ) { + $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); + break; + } } - // Create a temp FS file with the same extension and the thumbnail - $thumbExt = FileBackend::extensionFromPath( $thumbPath ); - $tmpFile = TempFSFile::factory( 'transform_', $thumbExt ); + $tmpFile = $this->makeTransformTmpFile( $thumbPath ); + if ( !$tmpFile ) { $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); - break; + } else { + $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags ); } - $tmpThumbPath = $tmpFile->getPath(); // path of 0-byte temp file - - // Actually render the thumbnail... - wfProfileIn( __METHOD__ . '-doTransform' ); - $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $params ); - wfProfileOut( __METHOD__ . '-doTransform' ); - $tmpFile->bind( $thumb ); // keep alive with $thumb - - if ( !$thumb ) { // bad params? - $thumb = null; - } elseif ( $thumb->isError() ) { // transform error - $this->lastError = $thumb->toText(); - // Ignore errors if requested - if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) { - $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $params ); - } - } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) { - // Copy the thumbnail from the file system into storage... - $disposition = $this->getThumbDisposition( $thumbName ); - $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition ); - if ( $status->isOK() ) { - $thumb->setStoragePath( $thumbPath ); - } else { - $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ); + } while ( false ); + + wfProfileOut( __METHOD__ ); + + return is_object( $thumb ) ? $thumb : false; + } + + /** + * Generates a thumbnail according to the given parameters and saves it to storage + * @param TempFSFile $tmpFile Temporary file where the rendered thumbnail will be saved + * @param array $transformParams + * @param int $flags + * @return bool|MediaTransformOutput + */ + public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) { + global $wgUseSquid, $wgIgnoreImageErrors; + + $handler = $this->getHandler(); + + $normalisedParams = $transformParams; + $handler->normaliseParams( $this, $normalisedParams ); + + $thumbName = $this->thumbName( $normalisedParams ); + $thumbUrl = $this->getThumbUrl( $thumbName ); + $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path + + $tmpThumbPath = $tmpFile->getPath(); + + if ( $handler->supportsBucketing() ) { + $this->generateBucketsIfNeeded( $normalisedParams, $flags ); + } + + // Actually render the thumbnail... + wfProfileIn( __METHOD__ . '-doTransform' ); + $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams ); + wfProfileOut( __METHOD__ . '-doTransform' ); + $tmpFile->bind( $thumb ); // keep alive with $thumb + + if ( !$thumb ) { // bad params? + $thumb = false; + } elseif ( $thumb->isError() ) { // transform error + $this->lastError = $thumb->toText(); + // Ignore errors if requested + if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) { + $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams ); + } + } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) { + // Copy the thumbnail from the file system into storage... + $disposition = $this->getThumbDisposition( $thumbName ); + $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition ); + if ( $status->isOK() ) { + $thumb->setStoragePath( $thumbPath ); + } else { + $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags ); + } + // Give extensions a chance to do something with this thumbnail... + wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) ); + } + + // Purge. Useful in the event of Core -> Squid connection failure or squid + // purge collisions from elsewhere during failure. Don't keep triggering for + // "thumbs" which have the main image URL though (bug 13776) + if ( $wgUseSquid ) { + if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) { + SquidUpdate::purge( array( $thumbUrl ) ); + } + } + + return $thumb; + } + + /** + * Generates chained bucketed thumbnails if needed + * @param array $params + * @param int $flags + * @return bool Whether at least one bucket was generated + */ + protected function generateBucketsIfNeeded( $params, $flags = 0 ) { + if ( !$this->repo + || !isset( $params['physicalWidth'] ) + || !isset( $params['physicalHeight'] ) + || !( $bucket = $this->getThumbnailBucket( $params['physicalWidth'] ) ) + || $bucket == $params['physicalWidth'] ) { + return false; + } + + $bucketPath = $this->getBucketThumbPath( $bucket ); + + if ( $this->repo->fileExists( $bucketPath ) ) { + return false; + } + + $params['physicalWidth'] = $bucket; + $params['width'] = $bucket; + + $params = $this->getHandler()->sanitizeParamsForBucketing( $params ); + + $bucketName = $this->getBucketThumbName( $bucket ); + + $tmpFile = $this->makeTransformTmpFile( $bucketPath ); + + if ( !$tmpFile ) { + return false; + } + + $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags ); + + if ( !$thumb || $thumb->isError() ) { + return false; + } + + $this->tmpBucketedThumbCache[$bucket] = $tmpFile->getPath(); + // For the caching to work, we need to make the tmp file survive as long as + // this object exists + $tmpFile->bind( $this ); + + return true; + } + + /** + * Returns the most appropriate source image for the thumbnail, given a target thumbnail size + * @param array $params + * @return array Source path and width/height of the source + */ + public function getThumbnailSource( $params ) { + if ( $this->repo + && $this->getHandler()->supportsBucketing() + && isset( $params['physicalWidth'] ) + && $bucket = $this->getThumbnailBucket( $params['physicalWidth'] ) + ) { + if ( $this->getWidth() != 0 ) { + $bucketHeight = round( $this->getHeight() * ( $bucket / $this->getWidth() ) ); + } else { + $bucketHeight = 0; + } + + // Try to avoid reading from storage if the file was generated by this script + if ( isset( $this->tmpBucketedThumbCache[$bucket] ) ) { + $tmpPath = $this->tmpBucketedThumbCache[$bucket]; + + if ( file_exists( $tmpPath ) ) { + return array( + 'path' => $tmpPath, + 'width' => $bucket, + 'height' => $bucketHeight + ); } - // Give extensions a chance to do something with this thumbnail... - wfRunHooks( 'FileTransformed', array( $this, $thumb, $tmpThumbPath, $thumbPath ) ); } - // Purge. Useful in the event of Core -> Squid connection failure or squid - // purge collisions from elsewhere during failure. Don't keep triggering for - // "thumbs" which have the main image URL though (bug 13776) - if ( $wgUseSquid ) { - if ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL() ) { - SquidUpdate::purge( array( $thumbUrl ) ); + $bucketPath = $this->getBucketThumbPath( $bucket ); + + if ( $this->repo->fileExists( $bucketPath ) ) { + $fsFile = $this->repo->getLocalReference( $bucketPath ); + + if ( $fsFile ) { + return array( + 'path' => $fsFile->getPath(), + 'width' => $bucket, + 'height' => $bucketHeight + ); } } - } while ( false ); + } - wfProfileOut( __METHOD__ ); - return is_object( $thumb ) ? $thumb : false; + // Thumbnailing a very large file could result in network saturation if + // everyone does it at once. + if ( $this->getSize() >= 1e7 ) { // 10MB + $that = $this; + $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $this->getName() ), + array( + 'doWork' => function() use ( $that ) { + return $that->getLocalRefPath(); + } + ) + ); + $srcPath = $work->execute(); + } else { + $srcPath = $this->getLocalRefPath(); + } + + // Original file + return array( + 'path' => $srcPath, + 'width' => $this->getWidth(), + 'height' => $this->getHeight() + ); + } + + /** + * Returns the repo path of the thumb for a given bucket + * @param int $bucket + * @return string + */ + protected function getBucketThumbPath( $bucket ) { + $thumbName = $this->getBucketThumbName( $bucket ); + return $this->getThumbPath( $thumbName ); + } + + /** + * Returns the name of the thumb for a given bucket + * @param int $bucket + * @return string + */ + protected function getBucketThumbName( $bucket ) { + return $this->thumbName( array( 'physicalWidth' => $bucket ) ); + } + + /** + * Creates a temp FS file with the same extension and the thumbnail + * @param string $thumbPath Thumbnail path + * @return TempFSFile + */ + protected function makeTransformTmpFile( $thumbPath ) { + $thumbExt = FileBackend::extensionFromPath( $thumbPath ); + return TempFSFile::factory( 'transform_', $thumbExt ); } /** * @param string $thumbName Thumbnail name + * @param string $dispositionType Type of disposition (either "attachment" or "inline") * @return string Content-Disposition header value */ - function getThumbDisposition( $thumbName ) { + function getThumbDisposition( $thumbName, $dispositionType = 'inline' ) { $fileName = $this->name; // file name to suggest $thumbExt = FileBackend::extensionFromPath( $thumbName ); if ( $thumbExt != '' && $thumbExt !== $this->getExtension() ) { $fileName .= ".$thumbExt"; } - return FileBackend::makeContentDisposition( 'inline', $fileName ); + + return FileBackend::makeContentDisposition( $dispositionType, $fileName ); } /** * Hook into transform() to allow migration of thumbnail files * STUB * Overridden by LocalFile + * @param string $thumbName */ - function migrateThumbFile( $thumbName ) {} + function migrateThumbFile( $thumbName ) { + } /** * Get a MediaHandler instance for this file * - * @return MediaHandler|boolean Registered MediaHandler for file's mime type or false if none found + * @return MediaHandler|bool Registered MediaHandler for file's MIME type + * or false if none found */ function getHandler() { if ( !isset( $this->handler ) ) { $this->handler = MediaHandler::getHandler( $this->getMimeType() ); } + return $this->handler; } @@ -1016,23 +1328,26 @@ abstract class File { * @return ThumbnailImage */ function iconThumb() { - global $wgStylePath, $wgStyleDirectory; + global $wgResourceBasePath, $IP; + $assetsPath = "$wgResourceBasePath/resources/assets/file-type-icons/"; + $assetsDirectory = "$IP/resources/assets/file-type-icons/"; $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ); foreach ( $try as $icon ) { - $path = '/common/images/icons/' . $icon; - $filepath = $wgStyleDirectory . $path; - if ( file_exists( $filepath ) ) { // always FS + if ( file_exists( $assetsDirectory . $icon ) ) { // always FS $params = array( 'width' => 120, 'height' => 120 ); - return new ThumbnailImage( $this, $wgStylePath . $path, false, $params ); + + return new ThumbnailImage( $this, $assetsPath . $icon, false, $params ); } } + return null; } /** * Get last thumbnailing error. * Largely obsolete. + * @return string */ function getLastError() { return $this->lastError; @@ -1053,9 +1368,10 @@ abstract class File { * STUB * Overridden by LocalFile * @param array $options Options, which include: - * 'forThumbRefresh' : The purging is only to refresh thumbnails + * 'forThumbRefresh' : The purging is only to refresh thumbnails */ - function purgeCache( $options = array() ) {} + function purgeCache( $options = array() ) { + } /** * Purge the file description page, but don't go after @@ -1091,9 +1407,9 @@ abstract class File { * Return a fragment of the history of file. * * STUB - * @param $limit integer Limit of rows to return - * @param string $start timestamp Only revisions older than $start will be returned - * @param string $end timestamp Only revisions newer than $end will be returned + * @param int $limit Limit of rows to return + * @param string $start Only revisions older than $start will be returned + * @param string $end Only revisions newer than $end will be returned * @param bool $inc Include the endpoints of the time range * * @return array @@ -1121,7 +1437,8 @@ abstract class File { * STUB * Overridden in LocalFile. */ - public function resetHistory() {} + public function resetHistory() { + } /** * Get the filename hash component of the directory including trailing slash, @@ -1135,6 +1452,7 @@ abstract class File { $this->assertRepoDefined(); $this->hashPath = $this->repo->getHashPath( $this->getName() ); } + return $this->hashPath; } @@ -1151,7 +1469,7 @@ abstract class File { /** * Get the path of an archived file relative to the public zone root * - * @param bool|string $suffix if not false, the name of an archived thumbnail file + * @param bool|string $suffix If not false, the name of an archived thumbnail file * * @return string */ @@ -1162,6 +1480,7 @@ abstract class File { } else { $path .= $suffix; } + return $path; } @@ -1169,8 +1488,7 @@ abstract class File { * Get the path, relative to the thumbnail zone root, of the * thumbnail directory or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getThumbRel( $suffix = false ) { @@ -1178,6 +1496,7 @@ abstract class File { if ( $suffix !== false ) { $path .= '/' . $suffix; } + return $path; } @@ -1195,9 +1514,8 @@ abstract class File { * Get the path, relative to the thumbnail zone root, for an archived file's thumbs directory * or a specific thumb if the $suffix is given. * - * @param string $archiveName the timestamped name of an archived image - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param string $archiveName The timestamped name of an archived image + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getArchiveThumbRel( $archiveName, $suffix = false ) { @@ -1207,64 +1525,64 @@ abstract class File { } else { $path .= $suffix; } + return $path; } /** * Get the path of the archived file. * - * @param bool|string $suffix if not false, the name of an archived file. - * + * @param bool|string $suffix If not false, the name of an archived file. * @return string */ function getArchivePath( $suffix = false ) { $this->assertRepoDefined(); + return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix ); } /** * Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified * - * @param string $archiveName the timestamped name of an archived image - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param string $archiveName The timestamped name of an archived image + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getArchiveThumbPath( $archiveName, $suffix = false ) { $this->assertRepoDefined(); + return $this->repo->getZonePath( 'thumb' ) . '/' . - $this->getArchiveThumbRel( $archiveName, $suffix ); + $this->getArchiveThumbRel( $archiveName, $suffix ); } /** * Get the path of the thumbnail directory, or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getThumbPath( $suffix = false ) { $this->assertRepoDefined(); + return $this->repo->getZonePath( 'thumb' ) . '/' . $this->getThumbRel( $suffix ); } /** * Get the path of the transcoded directory, or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of a media file - * + * @param bool|string $suffix If not false, the name of a media file * @return string */ function getTranscodedPath( $suffix = false ) { $this->assertRepoDefined(); + return $this->repo->getZonePath( 'transcoded' ) . '/' . $this->getThumbRel( $suffix ); } /** * Get the URL of the archive directory, or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of an archived file - * + * @param bool|string $suffix If not false, the name of an archived file * @return string */ function getArchiveUrl( $suffix = false ) { @@ -1276,15 +1594,15 @@ abstract class File { } else { $path .= rawurlencode( $suffix ); } + return $path; } /** * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified * - * @param string $archiveName the timestamped name of an archived image - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param string $archiveName The timestamped name of an archived image + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getArchiveThumbUrl( $archiveName, $suffix = false ) { @@ -1297,16 +1615,16 @@ abstract class File { } else { $path .= rawurlencode( $suffix ); } + return $path; } /** * Get the URL of the zone directory, or a particular file if $suffix is specified * - * @param string $zone name of requested zone - * @param bool|string $suffix if not false, the name of a file in zone - * - * @return string path + * @param string $zone Name of requested zone + * @param bool|string $suffix If not false, the name of a file in zone + * @return string Path */ function getZoneUrl( $zone, $suffix = false ) { $this->assertRepoDefined(); @@ -1315,15 +1633,15 @@ abstract class File { if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } + return $path; } /** * Get the URL of the thumbnail directory, or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of a thumbnail file - * - * @return string path + * @param bool|string $suffix If not false, the name of a thumbnail file + * @return string Path */ function getThumbUrl( $suffix = false ) { return $this->getZoneUrl( 'thumb', $suffix ); @@ -1332,9 +1650,8 @@ abstract class File { /** * Get the URL of the transcoded directory, or a particular file if $suffix is specified * - * @param bool|string $suffix if not false, the name of a media file - * - * @return string path + * @param bool|string $suffix If not false, the name of a media file + * @return string Path */ function getTranscodedUrl( $suffix = false ) { return $this->getZoneUrl( 'transcoded', $suffix ); @@ -1343,8 +1660,7 @@ abstract class File { /** * Get the public zone virtual URL for a current version source file * - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getVirtualUrl( $suffix = false ) { @@ -1353,14 +1669,14 @@ abstract class File { if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } + return $path; } /** * Get the public zone virtual URL for an archived version source file * - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getArchiveVirtualUrl( $suffix = false ) { @@ -1371,14 +1687,14 @@ abstract class File { } else { $path .= rawurlencode( $suffix ); } + return $path; } /** * Get the virtual URL for a thumbnail file or directory * - * @param bool|string $suffix if not false, the name of a thumbnail file - * + * @param bool|string $suffix If not false, the name of a thumbnail file * @return string */ function getThumbVirtualUrl( $suffix = false ) { @@ -1387,6 +1703,7 @@ abstract class File { if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } + return $path; } @@ -1395,6 +1712,7 @@ abstract class File { */ function isHashed() { $this->assertRepoDefined(); + return (bool)$this->repo->getHashLevels(); } @@ -1409,18 +1727,20 @@ abstract class File { * Record a file upload in the upload log and the image table * STUB * Overridden by LocalFile - * @param $oldver - * @param $desc - * @param $license string - * @param $copyStatus string - * @param $source string - * @param $watch bool - * @param $timestamp string|bool - * @param $user User object or null to use $wgUser + * @param string $oldver + * @param string $desc + * @param string $license + * @param string $copyStatus + * @param string $source + * @param bool $watch + * @param string|bool $timestamp + * @param null|User $user User object or null to use $wgUser * @return bool * @throws MWException */ - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false, User $user = null ) { + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', + $watch = false, $timestamp = false, User $user = null + ) { $this->readOnlyError(); } @@ -1435,13 +1755,12 @@ abstract class File { * Options to $options include: * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests * - * @param string $srcPath local filesystem path to the source image - * @param $flags Integer: a bitwise combination of: - * File::DELETE_SOURCE Delete the source file, i.e. move - * rather than copy + * @param string $srcPath Local filesystem path to the source image + * @param int $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy * @param array $options Optional additional parameters - * @return FileRepoStatus object. On success, the value member contains the - * archive name, or an empty string if it was a new file. + * @return FileRepoStatus On success, the value member contains the + * archive name, or an empty string if it was a new file. * * STUB * Overridden by LocalFile @@ -1457,6 +1776,7 @@ abstract class File { if ( !$this->getHandler() ) { return false; } + return $this->getHandler()->formatMetadata( $this, $this->getMetadata() ); } @@ -1481,7 +1801,7 @@ abstract class File { /** * Returns the repository * - * @return FileRepo|bool + * @return FileRepo|LocalRepo|bool */ function getRepo() { return $this->repo; @@ -1501,8 +1821,7 @@ abstract class File { * Is this file a "deleted" file in a private archive? * STUB * - * @param integer $field one of DELETED_* bitfield constants - * + * @param int $field One of DELETED_* bitfield constants * @return bool */ function isDeleted( $field ) { @@ -1525,6 +1844,7 @@ abstract class File { */ function wasDeleted() { $title = $this->getTitle(); + return $title && $title->isDeletedQuick(); } @@ -1537,8 +1857,8 @@ abstract class File { * Cache purging is done; checks for validity * and logging are caller's responsibility * - * @param $target Title New file name - * @return FileRepoStatus object. + * @param Title $target New file name + * @return FileRepoStatus */ function move( $target ) { $this->readOnlyError(); @@ -1552,13 +1872,14 @@ abstract class File { * * Cache purging is done; logging is caller's responsibility. * - * @param $reason String - * @param $suppress Boolean: hide content from sysops? - * @return bool on success, false on some kind of failure + * @param string $reason + * @param bool $suppress Hide content from sysops? + * @param User|null $user + * @return bool Boolean on success, false on some kind of failure * STUB * Overridden by LocalFile */ - function delete( $reason, $suppress = false ) { + function delete( $reason, $suppress = false, $user = null ) { $this->readOnlyError(); } @@ -1568,11 +1889,11 @@ abstract class File { * * May throw database exceptions on error. * - * @param array $versions set of record ids of deleted items to restore, - * or empty to restore all revisions. - * @param bool $unsuppress remove restrictions on content upon restoration? - * @return int|bool the number of file revisions restored if successful, - * or false on failure + * @param array $versions Set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @param bool $unsuppress Remove restrictions on content upon restoration? + * @return int|bool The number of file revisions restored if successful, + * or false on failure * STUB * Overridden by LocalFile */ @@ -1585,7 +1906,7 @@ abstract class File { * e.g. DJVU or PDF. Note that this may be true even if the file in * question only has a single page. * - * @return Bool + * @return bool */ function isMultipage() { return $this->getHandler() && $this->handler->isMultiPage( $this ); @@ -1605,15 +1926,16 @@ abstract class File { $this->pageCount = false; } } + return $this->pageCount; } /** * Calculate the height of a thumbnail using the source and destination width * - * @param $srcWidth - * @param $srcHeight - * @param $dstWidth + * @param int $srcWidth + * @param int $srcHeight + * @param int $dstWidth * * @return int */ @@ -1628,16 +1950,20 @@ abstract class File { /** * Get an image size array like that returned by getImageSize(), or false if it - * can't be determined. + * can't be determined. Loads the image size directly from the file ignoring caches. * - * @param string $fileName The filename - * @return Array + * @note Use getWidth()/getHeight() instead of this method unless you have a + * a good reason. This method skips all caches. + * + * @param string $filePath The path to the file (e.g. From getLocalPathRef() ) + * @return array The width, followed by height, with optionally more things after */ - function getImageSize( $fileName ) { + function getImageSize( $filePath ) { if ( !$this->getHandler() ) { return false; } - return $this->handler->getImageSize( $this, $fileName ); + + return $this->getHandler()->getImageSize( $this, $filePath ); } /** @@ -1657,7 +1983,7 @@ abstract class File { /** * Get the HTML text of the description page, if available * - * @param $lang Language Optional language to fetch description in + * @param bool|Language $lang Optional language to fetch description in * @return string */ function getDescriptionText( $lang = false ) { @@ -1672,11 +1998,16 @@ abstract class File { if ( $renderUrl ) { if ( $this->repo->descriptionCacheExpiry > 0 ) { wfDebug( "Attempting to get the description from cache..." ); - $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $lang->getCode(), - $this->getName() ); + $key = $this->repo->getLocalCacheKey( + 'RemoteFileDescription', + 'url', + $lang->getCode(), + $this->getName() + ); $obj = $wgMemc->get( $key ); if ( $obj ) { wfDebug( "success!\n" ); + return $obj; } wfDebug( "miss\n" ); @@ -1686,6 +2017,7 @@ abstract class File { if ( $res && $this->repo->descriptionCacheExpiry > 0 ) { $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry ); } + return $res; } else { return false; @@ -1696,12 +2028,12 @@ abstract class File { * Get description of file revision * STUB * - * @param $audience Integer: one of: - * File::FOR_PUBLIC to be displayed to all users - * File::FOR_THIS_USER to be displayed to the given user - * File::RAW get the description regardless of permissions - * @param $user User object to check for, only if FOR_THIS_USER is passed - * to the $audience parameter + * @param int $audience One of: + * File::FOR_PUBLIC to be displayed to all users + * File::FOR_THIS_USER to be displayed to the given user + * File::RAW get the description regardless of permissions + * @param User $user User object to check for, only if FOR_THIS_USER is + * passed to the $audience parameter * @return string */ function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { @@ -1715,6 +2047,7 @@ abstract class File { */ function getTimestamp() { $this->assertRepoDefined(); + return $this->repo->getFileTimestamp( $this->getPath() ); } @@ -1725,6 +2058,7 @@ abstract class File { */ function getSha1() { $this->assertRepoDefined(); + return $this->repo->getFileSha1( $this->getPath() ); } @@ -1740,6 +2074,7 @@ abstract class File { } $ext = $this->getExtension(); $dotExt = $ext === '' ? '' : ".$ext"; + return $hash . $dotExt; } @@ -1747,53 +2082,16 @@ abstract class File { * Determine if the current user is allowed to view a particular * field of this file, if it's marked as deleted. * STUB - * @param $field Integer - * @param $user User object to check, or null to use $wgUser - * @return Boolean + * @param int $field + * @param User $user User object to check, or null to use $wgUser + * @return bool */ function userCan( $field, User $user = null ) { return true; } /** - * Get an associative array containing information about a file in the local filesystem. - * - * @param string $path absolute local filesystem path - * @param $ext Mixed: the file extension, or true to extract it from the filename. - * Set it to false to ignore the extension. - * - * @return array - * @deprecated since 1.19 - */ - static function getPropsFromPath( $path, $ext = true ) { - wfDebug( __METHOD__ . ": Getting file info for $path\n" ); - wfDeprecated( __METHOD__, '1.19' ); - - $fsFile = new FSFile( $path ); - return $fsFile->getProps(); - } - - /** - * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case - * encoding, zero padded to 31 digits. - * - * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36 - * fairly neatly. - * - * @param $path string - * - * @return bool|string False on failure - * @deprecated since 1.19 - */ - static function sha1Base36( $path ) { - wfDeprecated( __METHOD__, '1.19' ); - - $fsFile = new FSFile( $path ); - return $fsFile->getSha1Base36(); - } - - /** - * @return Array HTTP header name/value map to use for HEAD/GET request responses + * @return array HTTP header name/value map to use for HEAD/GET request responses */ function getStreamHeaders() { $handler = $this->getHandler(); @@ -1841,7 +2139,7 @@ abstract class File { } /** - * @return + * @return string */ function getRedirected() { return $this->redirected; @@ -1855,13 +2153,15 @@ abstract class File { if ( !$this->redirectTitle ) { $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected ); } + return $this->redirectTitle; } + return null; } /** - * @param $from + * @param string $from * @return void */ function redirectedFrom( $from ) { @@ -1877,7 +2177,7 @@ abstract class File { /** * Check if this file object is small and can be cached - * @return boolean + * @return bool */ public function isCacheable() { return true; @@ -1902,4 +2202,13 @@ abstract class File { throw new MWException( "A Title object is not set for this File.\n" ); } } + + /** + * True if creating thumbnails from the file is large or otherwise resource-intensive. + * @return bool + */ + public function isExpensiveToThumbnail() { + $handler = $this->getHandler(); + return $handler ? $handler->isExpensiveToThumbnail( $this ) : false; + } } diff --git a/includes/filerepo/file/ForeignAPIFile.php b/includes/filerepo/file/ForeignAPIFile.php index ed96d446..3d5d5d60 100644 --- a/includes/filerepo/file/ForeignAPIFile.php +++ b/includes/filerepo/file/ForeignAPIFile.php @@ -33,9 +33,9 @@ class ForeignAPIFile extends File { protected $repoClass = 'ForeignApiRepo'; /** - * @param $title - * @param $repo ForeignApiRepo - * @param $info + * @param Title|string|bool $title + * @param ForeignApiRepo $repo + * @param array $info * @param bool $exists */ function __construct( $title, $repo, $info, $exists = false ) { @@ -48,8 +48,8 @@ class ForeignAPIFile extends File { } /** - * @param $title Title - * @param $repo ForeignApiRepo + * @param Title $title + * @param ForeignApiRepo $repo * @return ForeignAPIFile|null */ static function newFromTitle( Title $title, $repo ) { @@ -57,7 +57,10 @@ class ForeignAPIFile extends File { 'titles' => 'File:' . $title->getDBkey(), 'iiprop' => self::getProps(), 'prop' => 'imageinfo', - 'iimetadataversion' => MediaHandler::getMetadataVersion() + 'iimetadataversion' => MediaHandler::getMetadataVersion(), + // extmetadata is language-dependant, accessing the current language here + // would be problematic, so we just get them all + 'iiextmetadatamultilang' => 1, ) ); $info = $repo->getImageInfo( $data ); @@ -75,6 +78,7 @@ class ForeignAPIFile extends File { } else { $img = new self( $title, $repo, $info, true ); } + return $img; } else { return null; @@ -86,7 +90,7 @@ class ForeignAPIFile extends File { * @return string */ static function getProps() { - return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype'; + return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype|extmetadata'; } // Dummy functions... @@ -130,6 +134,7 @@ class ForeignAPIFile extends File { ); if ( $thumbUrl === false ) { global $wgLang; + return $this->repo->getThumbError( $this->getName(), $width, @@ -138,13 +143,14 @@ class ForeignAPIFile extends File { $wgLang->getCode() ); } + return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params ); } // Info we can get from API... /** - * @param $page int + * @param int $page * @return int|number */ public function getWidth( $page = 1 ) { @@ -152,7 +158,7 @@ class ForeignAPIFile extends File { } /** - * @param $page int + * @param int $page * @return int */ public function getHeight( $page = 1 ) { @@ -166,11 +172,24 @@ class ForeignAPIFile extends File { if ( isset( $this->mInfo['metadata'] ) ) { return serialize( self::parseMetadata( $this->mInfo['metadata'] ) ); } + return null; } /** - * @param $metadata array + * @return array|null Extended metadata (see imageinfo API for format) or + * null on error + */ + public function getExtendedMetadata() { + if ( isset( $this->mInfo['extmetadata'] ) ) { + return $this->mInfo['extmetadata']; + } + + return null; + } + + /** + * @param array $metadata * @return array */ public static function parseMetadata( $metadata ) { @@ -181,6 +200,7 @@ class ForeignAPIFile extends File { foreach ( $metadata as $meta ) { $ret[$meta['name']] = self::parseMetadata( $meta['value'] ); } + return $ret; } @@ -207,6 +227,8 @@ class ForeignAPIFile extends File { } /** + * @param int $audience + * @param User $user * @return null|string */ public function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { @@ -214,7 +236,7 @@ class ForeignAPIFile extends File { } /** - * @return null|String + * @return null|string */ function getSha1() { return isset( $this->mInfo['sha1'] ) @@ -223,7 +245,7 @@ class ForeignAPIFile extends File { } /** - * @return bool|Mixed|string + * @return bool|string */ function getTimestamp() { return wfTimestamp( TS_MW, @@ -241,6 +263,7 @@ class ForeignAPIFile extends File { $magic = MimeMagic::singleton(); $this->mInfo['mime'] = $magic->guessTypesForExtension( $this->getExtension() ); } + return $this->mInfo['mime']; } @@ -252,6 +275,7 @@ class ForeignAPIFile extends File { return $this->mInfo['mediatype']; } $magic = MimeMagic::singleton(); + return $magic->getMediaType( null, $this->getMimeType() ); } @@ -266,7 +290,7 @@ class ForeignAPIFile extends File { /** * Only useful if we're locally caching thumbs anyway... - * @param $suffix string + * @param string $suffix * @return null|string */ function getThumbPath( $suffix = '' ) { @@ -275,6 +299,7 @@ class ForeignAPIFile extends File { if ( $suffix ) { $path = $path . $suffix . '/'; } + return $path; } else { return null; @@ -314,7 +339,7 @@ class ForeignAPIFile extends File { } /** - * @param $options array + * @param array $options */ function purgeThumbnails( $options = array() ) { global $wgMemc; diff --git a/includes/filerepo/file/ForeignDBFile.php b/includes/filerepo/file/ForeignDBFile.php index 01d6b0f5..561ead75 100644 --- a/includes/filerepo/file/ForeignDBFile.php +++ b/includes/filerepo/file/ForeignDBFile.php @@ -27,11 +27,10 @@ * @ingroup FileAbstraction */ class ForeignDBFile extends LocalFile { - /** - * @param $title - * @param $repo - * @param $unused + * @param Title $title + * @param FileRepo $repo + * @param null $unused * @return ForeignDBFile */ static function newFromTitle( $title, $repo, $unused = null ) { @@ -42,23 +41,23 @@ class ForeignDBFile extends LocalFile { * Create a ForeignDBFile from a title * Do not call this except from inside a repo class. * - * @param $row - * @param $repo - * + * @param stdClass $row + * @param FileRepo $repo * @return ForeignDBFile */ static function newFromRow( $row, $repo ) { $title = Title::makeTitle( NS_FILE, $row->img_name ); $file = new self( $title, $repo ); $file->loadFromRow( $row ); + return $file; } /** - * @param $srcPath String - * @param $flags int - * @param $options Array - * @return \FileRepoStatus + * @param string $srcPath + * @param int $flags + * @param array $options + * @return FileRepoStatus * @throws MWException */ function publish( $srcPath, $flags = 0, array $options = array() ) { @@ -66,14 +65,14 @@ class ForeignDBFile extends LocalFile { } /** - * @param $oldver - * @param $desc string - * @param $license string - * @param $copyStatus string - * @param $source string - * @param $watch bool - * @param $timestamp bool|string - * @param $user User object or null to use $wgUser + * @param string $oldver + * @param string $desc + * @param string $license + * @param string $copyStatus + * @param string $source + * @param bool $watch + * @param bool|string $timestamp + * @param User $user User object or null to use $wgUser * @return bool * @throws MWException */ @@ -83,9 +82,9 @@ class ForeignDBFile extends LocalFile { } /** - * @param $versions array - * @param $unsuppress bool - * @return \FileRepoStatus + * @param array $versions + * @param bool $unsuppress + * @return FileRepoStatus * @throws MWException */ function restore( $versions = array(), $unsuppress = false ) { @@ -93,18 +92,19 @@ class ForeignDBFile extends LocalFile { } /** - * @param $reason string - * @param $suppress bool - * @return \FileRepoStatus + * @param string $reason + * @param bool $suppress + * @param User|null $user + * @return FileRepoStatus * @throws MWException */ - function delete( $reason, $suppress = false ) { + function delete( $reason, $suppress = false, $user = null ) { $this->readOnlyError(); } /** - * @param $target Title - * @return \FileRepoStatus + * @param Title $target + * @return FileRepoStatus * @throws MWException */ function move( $target ) { @@ -120,7 +120,7 @@ class ForeignDBFile extends LocalFile { } /** - * @param $lang Language Optional language to fetch description in. + * @param bool|Language $lang Optional language to fetch description in. * @return string */ function getDescriptionText( $lang = false ) { diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index d18f42e4..8824b669 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -46,45 +46,88 @@ define( 'MW_FILE_VERSION', 9 ); class LocalFile extends File { const CACHE_FIELD_MAX_LEN = 1000; - /**#@+ - * @private - */ - var - $fileExists, # does the file exist on disk? (loadFromXxx) - $historyLine, # Number of line to return by nextHistoryLine() (constructor) - $historyRes, # result of the query for the file's history (nextHistoryLine) - $width, # \ - $height, # | - $bits, # --- returned by getimagesize (loadFromXxx) - $attr, # / - $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...) - $mime, # MIME type, determined by MimeMagic::guessMimeType - $major_mime, # Major mime type - $minor_mime, # Minor mime type - $size, # Size in bytes (loadFromXxx) - $metadata, # Handler-specific metadata - $timestamp, # Upload timestamp - $sha1, # SHA-1 base 36 content hash - $user, $user_text, # User, who uploaded the file - $description, # Description of current revision of the file - $dataLoaded, # Whether or not core data has been loaded from the database (loadFromXxx) - $extraDataLoaded, # Whether or not lazy-loaded data has been loaded from the database - $upgraded, # Whether the row was upgraded on load - $locked, # True if the image row is locked - $lockedOwnTrx, # True if the image row is locked with a lock initiated transaction - $missing, # True if file is not present in file system. Not to be cached in memcached - $deleted; # Bitfield akin to rev_deleted - - /**#@-*/ - - /** - * @var LocalRepo - */ - var $repo; + /** @var bool Does the file exist on disk? (loadFromXxx) */ + protected $fileExists; + /** @var int Image width */ + protected $width; + + /** @var int Image height */ + protected $height; + + /** @var int Returned by getimagesize (loadFromXxx) */ + protected $bits; + + /** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */ + protected $media_type; + + /** @var string MIME type, determined by MimeMagic::guessMimeType */ + protected $mime; + + /** @var int Size in bytes (loadFromXxx) */ + protected $size; + + /** @var string Handler-specific metadata */ + protected $metadata; + + /** @var string SHA-1 base 36 content hash */ + protected $sha1; + + /** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */ + protected $dataLoaded; + + /** @var bool Whether or not lazy-loaded data has been loaded from the database */ + protected $extraDataLoaded; + + /** @var int Bitfield akin to rev_deleted */ + protected $deleted; + + /** @var string */ protected $repoClass = 'LocalRepo'; + /** @var int Number of line to return by nextHistoryLine() (constructor) */ + private $historyLine; + + /** @var int Result of the query for the file's history (nextHistoryLine) */ + private $historyRes; + + /** @var string Major MIME type */ + private $major_mime; + + /** @var string Minor MIME type */ + private $minor_mime; + + /** @var string Upload timestamp */ + private $timestamp; + + /** @var int User ID of uploader */ + private $user; + + /** @var string User name of uploader */ + private $user_text; + + /** @var string Description of current revision of the file */ + private $description; + + /** @var bool Whether the row was upgraded on load */ + private $upgraded; + + /** @var bool True if the image row is locked */ + private $locked; + + /** @var bool True if the image row is locked with a lock initiated transaction */ + private $lockedOwnTrx; + + /** @var bool True if file is not present in file system. Not to be cached in memcached */ + private $missing; + + /** @var int UNIX timestamp of last markVolatile() call */ + private $lastMarkedVolatile = 0; + const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata) + const LOAD_VIA_SLAVE = 2; // integer; use a slave to load the data + + const VOLATILE_TTL = 300; // integer; seconds /** * Create a LocalFile from a title @@ -92,9 +135,9 @@ class LocalFile extends File { * * Note: $unused param is only here to avoid an E_STRICT * - * @param $title - * @param $repo - * @param $unused + * @param Title $title + * @param FileRepo $repo + * @param null $unused * * @return LocalFile */ @@ -106,8 +149,8 @@ class LocalFile extends File { * Create a LocalFile from a title * Do not call this except from inside a repo class. * - * @param $row - * @param $repo + * @param stdClass $row + * @param FileRepo $repo * * @return LocalFile */ @@ -123,10 +166,9 @@ class LocalFile extends File { * Create a LocalFile from a SHA-1 key * Do not call this except from inside a repo class. * - * @param string $sha1 base-36 SHA-1 - * @param $repo LocalRepo + * @param string $sha1 Base-36 SHA-1 + * @param LocalRepo $repo * @param string|bool $timestamp MW_timestamp (optional) - * * @return bool|LocalFile */ static function newFromKey( $sha1, $repo, $timestamp = false ) { @@ -171,6 +213,8 @@ class LocalFile extends File { /** * Constructor. * Do not call this except from inside a repo class. + * @param Title $title + * @param FileRepo $repo */ function __construct( $title, $repo ) { parent::__construct( $title, $repo ); @@ -188,7 +232,7 @@ class LocalFile extends File { /** * Get the memcached key for the main data for this file, or false if * there is no access to the shared cache. - * @return bool + * @return string|bool */ function getCacheKey() { $hashedName = md5( $this->getName() ); @@ -210,6 +254,7 @@ class LocalFile extends File { if ( !$key ) { wfProfileOut( __METHOD__ ); + return false; } @@ -236,6 +281,7 @@ class LocalFile extends File { } wfProfileOut( __METHOD__ ); + return $this->dataLoaded; } @@ -284,12 +330,13 @@ class LocalFile extends File { } /** - * @param $prefix string + * @param string $prefix * @return array */ function getCacheFields( $prefix = 'img_' ) { static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', - 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' ); + 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', + 'user_text', 'description' ); static $results = array(); if ( $prefix == '' ) { @@ -308,6 +355,7 @@ class LocalFile extends File { } /** + * @param string $prefix * @return array */ function getLazyCacheFields( $prefix = 'img_' ) { @@ -331,8 +379,9 @@ class LocalFile extends File { /** * Load file metadata from the DB + * @param int $flags */ - function loadFromDB() { + function loadFromDB( $flags = 0 ) { # Polymorphic function name to distinguish foreign and local fetches $fname = get_class( $this ) . '::' . __FUNCTION__; wfProfileIn( $fname ); @@ -341,7 +390,10 @@ class LocalFile extends File { $this->dataLoaded = true; $this->extraDataLoaded = true; - $dbr = $this->repo->getMasterDB(); + $dbr = ( $flags & self::LOAD_VIA_SLAVE ) + ? $this->repo->getSlaveDB() + : $this->repo->getMasterDB(); + $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), array( 'img_name' => $this->getName() ), $fname ); @@ -366,19 +418,13 @@ class LocalFile extends File { # Unconditionally set loaded=true, we don't want the accessors constantly rechecking $this->extraDataLoaded = true; - $dbr = $this->repo->getSlaveDB(); - // In theory the file could have just been renamed/deleted...oh well - $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), - array( 'img_name' => $this->getName() ), $fname ); - - if ( !$row ) { // fallback to master - $dbr = $this->repo->getMasterDB(); - $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), - array( 'img_name' => $this->getName() ), $fname ); + $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname ); + if ( !$fieldMap ) { + $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname ); } - if ( $row ) { - foreach ( $this->unprefixRow( $row, 'img_' ) as $name => $value ) { + if ( $fieldMap ) { + foreach ( $fieldMap as $name => $value ) { $this->$name = $value; } } else { @@ -390,9 +436,36 @@ class LocalFile extends File { } /** - * @param Row $row - * @param $prefix string - * @return Array + * @param DatabaseBase $dbr + * @param string $fname + * @return array|bool + */ + private function loadFieldsWithTimestamp( $dbr, $fname ) { + $fieldMap = false; + + $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), + array( 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ), + $fname ); + if ( $row ) { + $fieldMap = $this->unprefixRow( $row, 'img_' ); + } else { + # File may have been uploaded over in the meantime; check the old versions + $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ), + array( 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ), + $fname ); + if ( $row ) { + $fieldMap = $this->unprefixRow( $row, 'oi_' ); + } + } + + return $fieldMap; + } + + /** + * @param array $row Row + * @param string $prefix + * @throws MWException + * @return array */ protected function unprefixRow( $row, $prefix = 'img_' ) { $array = (array)$row; @@ -407,14 +480,15 @@ class LocalFile extends File { foreach ( $array as $name => $value ) { $decoded[substr( $name, $prefixLength )] = $value; } + return $decoded; } /** * Decode a row from the database (either object or array) to an array * with timestamps and MIME types decoded, and the field prefix removed. - * @param $row - * @param $prefix string + * @param object $row + * @param string $prefix * @throws MWException * @return array */ @@ -442,6 +516,9 @@ class LocalFile extends File { /** * Load file metadata from a DB result row + * + * @param object $row + * @param string $prefix */ function loadFromRow( $row, $prefix = 'img_' ) { $this->dataLoaded = true; @@ -459,12 +536,12 @@ class LocalFile extends File { /** * Load file metadata from cache or DB, unless already loaded - * @param integer $flags + * @param int $flags */ function load( $flags = 0 ) { if ( !$this->dataLoaded ) { if ( !$this->loadFromCache() ) { - $this->loadFromDB(); + $this->loadFromDB( $this->isVolatile() ? 0 : self::LOAD_VIA_SLAVE ); $this->saveToCache(); } $this->dataLoaded = true; @@ -518,8 +595,10 @@ class LocalFile extends File { # Don't destroy file info of missing files if ( !$this->fileExists ) { + $this->unlock(); wfDebug( __METHOD__ . ": file does not exist, aborting\n" ); wfProfileOut( __METHOD__ ); + return; } @@ -527,7 +606,9 @@ class LocalFile extends File { list( $major, $minor ) = self::splitMime( $this->mime ); if ( wfReadOnly() ) { + $this->unlock(); wfProfileOut( __METHOD__ ); + return; } wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" ); @@ -541,7 +622,7 @@ class LocalFile extends File { 'img_media_type' => $this->media_type, 'img_major_mime' => $major, 'img_minor_mime' => $minor, - 'img_metadata' => $dbw->encodeBlob($this->metadata), + 'img_metadata' => $dbw->encodeBlob( $this->metadata ), 'img_sha1' => $this->sha1, ), array( 'img_name' => $this->getName() ), @@ -562,6 +643,8 @@ class LocalFile extends File { * * If 'mime' is given, it will be split into major_mime/minor_mime. * If major_mime/minor_mime are given, $this->mime will also be set. + * + * @param array $info */ function setProps( $info ) { $this->dataLoaded = true; @@ -599,13 +682,14 @@ class LocalFile extends File { list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() ); $this->missing = !$fileExists; } + return $this->missing; } /** * Return the width of the image * - * @param $page int + * @param int $page * @return int */ public function getWidth( $page = 1 ) { @@ -632,7 +716,7 @@ class LocalFile extends File { /** * Return the height of the image * - * @param $page int + * @param int $page * @return int */ public function getHeight( $page = 1 ) { @@ -686,34 +770,38 @@ class LocalFile extends File { */ function getBitDepth() { $this->load(); + return $this->bits; } /** - * Return the size of the image file, in bytes + * Returns the size of the image file, in bytes * @return int */ public function getSize() { $this->load(); + return $this->size; } /** - * Returns the mime type of the file. + * Returns the MIME type of the file. * @return string */ function getMimeType() { $this->load(); + return $this->mime; } /** - * Return the type of the media in the file. + * Returns the type of the media in the file. * Use the value returned by this function with the MEDIATYPE_xxx constants. * @return string */ function getMediaType() { $this->load(); + return $this->media_type; } @@ -725,10 +813,11 @@ class LocalFile extends File { /** * Returns true if the file exists on disk. - * @return boolean Whether file exist on disk. + * @return bool Whether file exist on disk. */ public function exists() { $this->load(); + return $this->fileExists; } @@ -738,40 +827,6 @@ class LocalFile extends File { /** createThumb inherited */ /** transform inherited */ - /** - * Fix thumbnail files from 1.4 or before, with extreme prejudice - * @todo : do we still care about this? Perhaps a maintenance script - * can be made instead. Enabling this code results in a serious - * RTT regression for wikis without 404 handling. - */ - function migrateThumbFile( $thumbName ) { - /* Old code for bug 2532 - $thumbDir = $this->getThumbPath(); - $thumbPath = "$thumbDir/$thumbName"; - if ( is_dir( $thumbPath ) ) { - // Directory where file should be - // This happened occasionally due to broken migration code in 1.5 - // Rename to broken-* - for ( $i = 0; $i < 100; $i++ ) { - $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName"; - if ( !file_exists( $broken ) ) { - rename( $thumbPath, $broken ); - break; - } - } - // Doesn't exist anymore - clearstatcache(); - } - */ - - /* - if ( $this->repo->fileExists( $thumbDir ) ) { - // Delete file where directory should be - $this->repo->cleanupBatch( array( $thumbDir ) ); - } - */ - } - /** getHandler inherited */ /** iconThumb inherited */ /** getLastError inherited */ @@ -779,7 +834,7 @@ class LocalFile extends File { /** * Get all thumbnail names previously generated for this file * @param string|bool $archiveName Name of an archive file, default false - * @return array first element is the base dir, then files in that base dir. + * @return array First element is the base dir, then files in that base dir. */ function getThumbnails( $archiveName = false ) { if ( $archiveName ) { @@ -795,7 +850,8 @@ class LocalFile extends File { foreach ( $iterator as $file ) { $files[] = $file; } - } catch ( FileBackendError $e ) {} // suppress (bug 54674) + } catch ( FileBackendError $e ) { + } // suppress (bug 54674) return $files; } @@ -828,7 +884,7 @@ class LocalFile extends File { /** * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid. * - * @param Array $options An array potentially with the key forThumbRefresh. + * @param array $options An array potentially with the key forThumbRefresh. * * @note This used to purge old thumbnails by default as well, but doesn't anymore. */ @@ -847,7 +903,7 @@ class LocalFile extends File { /** * Delete cached transformed files for an archived version only. - * @param string $archiveName name of the archived file + * @param string $archiveName Name of the archived file */ function purgeOldThumbnails( $archiveName ) { global $wgUseSquid; @@ -855,12 +911,13 @@ class LocalFile extends File { // Get a list of old thumbnails and URLs $files = $this->getThumbnails( $archiveName ); - $dir = array_shift( $files ); - $this->purgeThumbList( $dir, $files ); // Purge any custom thumbnail caches wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, $archiveName ) ); + $dir = array_shift( $files ); + $this->purgeThumbList( $dir, $files ); + // Purge the squid if ( $wgUseSquid ) { $urls = array(); @@ -875,6 +932,7 @@ class LocalFile extends File { /** * Delete cached transformed files for the current version only. + * @param array $options */ function purgeThumbnails( $options = array() ) { global $wgUseSquid; @@ -883,8 +941,8 @@ class LocalFile extends File { // Delete thumbnails $files = $this->getThumbnails(); // Always purge all files from squid regardless of handler filters + $urls = array(); if ( $wgUseSquid ) { - $urls = array(); foreach ( $files as $file ) { $urls[] = $this->getThumbUrl( $file ); } @@ -899,12 +957,12 @@ class LocalFile extends File { } } - $dir = array_shift( $files ); - $this->purgeThumbList( $dir, $files ); - // Purge any custom thumbnail caches wfRunHooks( 'LocalFilePurgeThumbnails', array( $this, false ) ); + $dir = array_shift( $files ); + $this->purgeThumbList( $dir, $files ); + // Purge the squid if ( $wgUseSquid ) { SquidUpdate::purge( $urls ); @@ -915,8 +973,8 @@ class LocalFile extends File { /** * Delete a list of thumbnails visible at urls - * @param string $dir base dir of the files. - * @param array $files of strings: relative filenames (to $dir) + * @param string $dir Base dir of the files. + * @param array $files Array of strings: relative filenames (to $dir) */ protected function purgeThumbList( $dir, $files ) { $fileListDebug = strtr( @@ -946,10 +1004,10 @@ class LocalFile extends File { /** purgeEverything inherited */ /** - * @param $limit null - * @param $start null - * @param $end null - * @param $inc bool + * @param int $limit Optional: Limit to number of results + * @param int $start Optional: Timestamp, start from + * @param int $end Optional: Timestamp, end at + * @param bool $inc * @return array */ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) { @@ -984,11 +1042,7 @@ class LocalFile extends File { $r = array(); foreach ( $res as $row ) { - if ( $this->repo->oldFileFromRowFactory ) { - $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo ); - } else { - $r[] = OldLocalFile::newFromRow( $row, $this->repo ); - } + $r[] = $this->repo->newFileFromRow( $row ); } if ( $order == 'ASC' ) { @@ -999,7 +1053,7 @@ class LocalFile extends File { } /** - * Return the history of this file, line by line. + * Returns the history of this file, line by line. * starts with current version, then old versions. * uses $this->historyLine to check which line to return: * 0 return line for current version @@ -1013,7 +1067,7 @@ class LocalFile extends File { $dbr = $this->repo->getSlaveDB(); - if ( $this->historyLine == 0 ) {// called for the first time, return line from cur + if ( $this->historyLine == 0 ) { // called for the first time, return line from cur $this->historyRes = $dbr->select( 'image', array( '*', @@ -1027,6 +1081,7 @@ class LocalFile extends File { if ( 0 == $dbr->numRows( $this->historyRes ) ) { $this->historyRes = null; + return false; } } elseif ( $this->historyLine == 1 ) { @@ -1036,7 +1091,7 @@ class LocalFile extends File { array( 'ORDER BY' => 'oi_timestamp DESC' ) ); } - $this->historyLine ++; + $this->historyLine++; return $dbr->fetchObject( $this->historyRes ); } @@ -1066,21 +1121,24 @@ class LocalFile extends File { /** * Upload a file and record it in the DB - * @param string $srcPath source storage path, virtual URL, or filesystem path - * @param string $comment upload description - * @param string $pageText text to use for the new description page, - * if a new description page is created - * @param $flags Integer|bool: flags for publish() - * @param array|bool $props File properties, if known. This can be used to reduce the - * upload time when uploading virtual URLs for which the file info - * is already known - * @param string|bool $timestamp timestamp for img_timestamp, or false to use the current time - * @param $user User|null: User object or null to use $wgUser + * @param string $srcPath Source storage path, virtual URL, or filesystem path + * @param string $comment Upload description + * @param string $pageText Text to use for the new description page, + * if a new description page is created + * @param int|bool $flags Flags for publish() + * @param array|bool $props File properties, if known. This can be used to + * reduce the upload time when uploading virtual URLs for which the file + * info is already known + * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the + * current time + * @param User|null $user User object or null to use $wgUser * - * @return FileRepoStatus object. On success, the value member contains the + * @return FileRepoStatus On success, the value member contains the * archive name, or an empty string if it was a new file. */ - function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) { + function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, + $timestamp = false, $user = null + ) { global $wgContLang; if ( $this->getRepo()->getReadOnlyReason() !== false ) { @@ -1090,8 +1148,8 @@ class LocalFile extends File { if ( !$props ) { wfProfileIn( __METHOD__ . '-getProps' ); if ( $this->repo->isVirtualUrl( $srcPath ) - || FileBackend::isStoragePath( $srcPath ) ) - { + || FileBackend::isStoragePath( $srcPath ) + ) { $props = $this->repo->getFileProps( $srcPath ); } else { $props = FSFile::getPropsFromPath( $srcPath ); @@ -1110,16 +1168,19 @@ class LocalFile extends File { // Trim spaces on user supplied text $comment = trim( $comment ); - // truncate nicely or the DB will do it for us + // Truncate nicely or the DB will do it for us // non-nicely (dangling multi-byte chars, non-truncated version in cache). $comment = $wgContLang->truncate( $comment, 255 ); $this->lock(); // begin $status = $this->publish( $srcPath, $flags, $options ); - if ( $status->successCount > 0 ) { - # Essentially we are displacing any existing current file and saving - # a new current file at the old location. If just the first succeeded, - # we still need to displace the current DB entry and put in a new one. + if ( $status->successCount >= 2 ) { + // There will be a copy+(one of move,copy,store). + // The first succeeding does not commit us to updating the DB + // since it simply copied the current version to a timestamped file name. + // It is only *preferable* to avoid leaving such files orphaned. + // Once the second operation goes through, then the current version was + // updated and we must therefore update the DB too. if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) { $status->fatal( 'filenotfound', $srcPath ); } @@ -1132,19 +1193,18 @@ class LocalFile extends File { /** * Record a file upload in the upload log and the image table - * @param $oldver - * @param $desc string - * @param $license string - * @param $copyStatus string - * @param $source string - * @param $watch bool - * @param $timestamp string|bool - * @param $user User object or null to use $wgUser + * @param string $oldver + * @param string $desc + * @param string $license + * @param string $copyStatus + * @param string $source + * @param bool $watch + * @param string|bool $timestamp + * @param User|null $user User object or null to use $wgUser * @return bool */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', - $watch = false, $timestamp = false, User $user = null ) - { + $watch = false, $timestamp = false, User $user = null ) { if ( !$user ) { global $wgUser; $user = $wgUser; @@ -1159,21 +1219,22 @@ class LocalFile extends File { if ( $watch ) { $user->addWatch( $this->getTitle() ); } + return true; } /** * Record a file upload in the upload log and the image table - * @param $oldver - * @param $comment string - * @param $pageText string - * @param $props bool|array - * @param $timestamp bool|string - * @param $user null|User + * @param string $oldver + * @param string $comment + * @param string $pageText + * @param bool|array $props + * @param string|bool $timestamp + * @param null|User $user * @return bool */ - function recordUpload2( - $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null + function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, + $user = null ) { wfProfileIn( __METHOD__ ); @@ -1191,8 +1252,13 @@ class LocalFile extends File { wfProfileOut( __METHOD__ . '-getProps' ); } + # Imports or such might force a certain timestamp; otherwise we generate + # it and can fudge it slightly to keep (name,timestamp) unique on re-upload. if ( $timestamp === false ) { $timestamp = $dbw->timestamp(); + $allowTimeKludge = true; + } else { + $allowTimeKludge = false; } $props['description'] = $comment; @@ -1204,7 +1270,9 @@ class LocalFile extends File { # Fail now if the file isn't there if ( !$this->fileExists ) { wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" ); + $dbw->rollback( __METHOD__ ); wfProfileOut( __METHOD__ ); + return false; } @@ -1227,13 +1295,27 @@ class LocalFile extends File { 'img_description' => $comment, 'img_user' => $user->getId(), 'img_user_text' => $user->getName(), - 'img_metadata' => $dbw->encodeBlob($this->metadata), + 'img_metadata' => $dbw->encodeBlob( $this->metadata ), 'img_sha1' => $this->sha1 ), __METHOD__, 'IGNORE' ); if ( $dbw->affectedRows() == 0 ) { + if ( $allowTimeKludge ) { + # Use FOR UPDATE to ignore any transaction snapshotting + $ltimestamp = $dbw->selectField( 'image', 'img_timestamp', + array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) ); + $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false; + # Avoid a timestamp that is not newer than the last version + # TODO: the image/oldimage tables should be like page/revision with an ID field + if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) { + sleep( 1 ); // fast enough re-uploads would go far in the future otherwise + $timestamp = $dbw->timestamp( $lUnixtime + 1 ); + $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW + } + } + # (bug 34993) Note: $oldver can be empty here, if the previous # version of the file was broken. Allow registration of the new # version to continue anyway, because that's better than having @@ -1244,21 +1326,21 @@ class LocalFile extends File { # Insert previous contents into oldimage $dbw->insertSelect( 'oldimage', 'image', array( - 'oi_name' => 'img_name', + 'oi_name' => 'img_name', 'oi_archive_name' => $dbw->addQuotes( $oldver ), - 'oi_size' => 'img_size', - 'oi_width' => 'img_width', - 'oi_height' => 'img_height', - 'oi_bits' => 'img_bits', - 'oi_timestamp' => 'img_timestamp', - 'oi_description' => 'img_description', - 'oi_user' => 'img_user', - 'oi_user_text' => 'img_user_text', - 'oi_metadata' => 'img_metadata', - 'oi_media_type' => 'img_media_type', - 'oi_major_mime' => 'img_major_mime', - 'oi_minor_mime' => 'img_minor_mime', - 'oi_sha1' => 'img_sha1' + 'oi_size' => 'img_size', + 'oi_width' => 'img_width', + 'oi_height' => 'img_height', + 'oi_bits' => 'img_bits', + 'oi_timestamp' => 'img_timestamp', + 'oi_description' => 'img_description', + 'oi_user' => 'img_user', + 'oi_user_text' => 'img_user_text', + 'oi_metadata' => 'img_metadata', + 'oi_media_type' => 'img_media_type', + 'oi_major_mime' => 'img_major_mime', + 'oi_minor_mime' => 'img_minor_mime', + 'oi_sha1' => 'img_sha1' ), array( 'img_name' => $this->getName() ), __METHOD__ @@ -1267,19 +1349,19 @@ class LocalFile extends File { # Update the current image row $dbw->update( 'image', array( /* SET */ - 'img_size' => $this->size, - 'img_width' => intval( $this->width ), - 'img_height' => intval( $this->height ), - 'img_bits' => $this->bits, - 'img_media_type' => $this->media_type, - 'img_major_mime' => $this->major_mime, - 'img_minor_mime' => $this->minor_mime, - 'img_timestamp' => $timestamp, + 'img_size' => $this->size, + 'img_width' => intval( $this->width ), + 'img_height' => intval( $this->height ), + 'img_bits' => $this->bits, + 'img_media_type' => $this->media_type, + 'img_major_mime' => $this->major_mime, + 'img_minor_mime' => $this->minor_mime, + 'img_timestamp' => $timestamp, 'img_description' => $comment, - 'img_user' => $user->getId(), - 'img_user_text' => $user->getName(), - 'img_metadata' => $dbw->encodeBlob($this->metadata), - 'img_sha1' => $this->sha1 + 'img_user' => $user->getId(), + 'img_user_text' => $user->getName(), + 'img_metadata' => $dbw->encodeBlob( $this->metadata ), + 'img_sha1' => $this->sha1 ), array( 'img_name' => $this->getName() ), __METHOD__ @@ -1333,7 +1415,8 @@ class LocalFile extends File { $dbw, $descTitle->getArticleID(), $editSummary, - false + false, + $user ); if ( !is_null( $nullRevision ) ) { $nullRevision->insertOn( $dbw ); @@ -1349,6 +1432,12 @@ class LocalFile extends File { # to after $wikiPage->doEdit has been called. $dbw->commit( __METHOD__ ); + # Save to memcache. + # We shall not saveToCache before the commit since otherwise + # in case of a rollback there is an usable file from memcached + # which in fact doesn't really exist (bug 24978) + $this->saveToCache(); + if ( $exists ) { # Invalidate the cache for the description page $descTitle->invalidateCache(); @@ -1358,7 +1447,13 @@ class LocalFile extends File { # There's already a log entry, so don't make a second RC entry # Squid and file cache for the description page are purged by doEditContent. $content = ContentHandler::makeContent( $pageText, $descTitle ); - $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user ); + $status = $wikiPage->doEditContent( + $content, + $comment, + EDIT_NEW | EDIT_SUPPRESS_RC, + false, + $user + ); $dbw->begin( __METHOD__ ); // XXX; doEdit() uses a transaction // Now that the page exists, make an RC entry. @@ -1373,15 +1468,8 @@ class LocalFile extends File { $dbw->commit( __METHOD__ ); // commit before anything bad can happen } - wfProfileOut( __METHOD__ . '-edit' ); - # Save to cache and purge the squid - # We shall not saveToCache before the commit since otherwise - # in case of a rollback there is an usable file from memcached - # which in fact doesn't really exist (bug 24978) - $this->saveToCache(); - if ( $reupload ) { # Delete old thumbnails wfProfileIn( __METHOD__ . '-purge' ); @@ -1404,18 +1492,8 @@ class LocalFile extends File { LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' ); } - # Invalidate cache for all pages that redirects on this page - $redirs = $this->getTitle()->getRedirectsHere(); - - foreach ( $redirs as $redir ) { - if ( !$reupload && $redir->getNamespace() === NS_FILE ) { - LinksUpdate::queueRecursiveJobsForTable( $redir, 'imagelinks' ); - } - $update = new HTMLCacheUpdate( $redir, 'imagelinks' ); - $update->doUpdate(); - } - wfProfileOut( __METHOD__ ); + return true; } @@ -1427,11 +1505,11 @@ class LocalFile extends File { * The archive name should be passed through to recordUpload for database * registration. * - * @param string $srcPath local filesystem path to the source image - * @param $flags Integer: a bitwise combination of: - * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy + * @param string $srcPath Local filesystem path to the source image + * @param int $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy * @param array $options Optional additional parameters - * @return FileRepoStatus object. On success, the value member contains the + * @return FileRepoStatus On success, the value member contains the * archive name, or an empty string if it was a new file. */ function publish( $srcPath, $flags = 0, array $options = array() ) { @@ -1445,12 +1523,12 @@ class LocalFile extends File { * The archive name should be passed through to recordUpload for database * registration. * - * @param string $srcPath local filesystem path to the source image - * @param string $dstRel target relative path - * @param $flags Integer: a bitwise combination of: - * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy + * @param string $srcPath Local filesystem path to the source image + * @param string $dstRel Target relative path + * @param int $flags A bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy * @param array $options Optional additional parameters - * @return FileRepoStatus object. On success, the value member contains the + * @return FileRepoStatus On success, the value member contains the * archive name, or an empty string if it was a new file. */ function publishTo( $srcPath, $dstRel, $flags = 0, array $options = array() ) { @@ -1490,8 +1568,8 @@ class LocalFile extends File { * Cache purging is done; checks for validity * and logging are caller's responsibility * - * @param $target Title New file name - * @return FileRepoStatus object. + * @param Title $target New file name + * @return FileRepoStatus */ function move( $target ) { if ( $this->getRepo()->getReadOnlyReason() !== false ) { @@ -1515,7 +1593,7 @@ class LocalFile extends File { // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside. $this->getRepo()->getMasterDB()->onTransactionIdle( - function() use ( $oldTitleFile, $newTitleFile, $archiveNames ) { + function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) { $oldTitleFile->purgeEverything(); foreach ( $archiveNames as $archiveName ) { $oldTitleFile->purgeOldThumbnails( $archiveName ); @@ -1543,16 +1621,17 @@ class LocalFile extends File { * * Cache purging is done; logging is caller's responsibility. * - * @param $reason - * @param $suppress - * @return FileRepoStatus object. + * @param string $reason + * @param bool $suppress + * @param User|null $user + * @return FileRepoStatus */ - function delete( $reason, $suppress = false ) { + function delete( $reason, $suppress = false, $user = null ) { if ( $this->getRepo()->getReadOnlyReason() !== false ) { return $this->readOnlyFatalStatus(); } - $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user ); $this->lock(); // begin $batch->addCurrent(); @@ -1569,7 +1648,7 @@ class LocalFile extends File { // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside. $file = $this; $this->getRepo()->getMasterDB()->onTransactionIdle( - function() use ( $file, $archiveNames ) { + function () use ( $file, $archiveNames ) { global $wgUseSquid; $file->purgeEverything(); @@ -1599,19 +1678,20 @@ class LocalFile extends File { * * Cache purging is done; logging is caller's responsibility. * - * @param $archiveName String - * @param $reason String - * @param $suppress Boolean - * @throws MWException or FSException on database or file store failure - * @return FileRepoStatus object. + * @param string $archiveName + * @param string $reason + * @param bool $suppress + * @param User|null $user + * @throws MWException Exception on database or file store failure + * @return FileRepoStatus */ - function deleteOld( $archiveName, $reason, $suppress = false ) { + function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) { global $wgUseSquid; if ( $this->getRepo()->getReadOnlyReason() !== false ) { return $this->readOnlyFatalStatus(); } - $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user ); $this->lock(); // begin $batch->addOld( $archiveName ); @@ -1638,9 +1718,9 @@ class LocalFile extends File { * * May throw database exceptions on error. * - * @param array $versions set of record ids of deleted items to restore, - * or empty to restore all revisions. - * @param $unsuppress Boolean + * @param array $versions Set of record ids of deleted items to restore, + * or empty to restore all revisions. + * @param bool $unsuppress * @return FileRepoStatus */ function restore( $versions = array(), $unsuppress = false ) { @@ -1675,7 +1755,7 @@ class LocalFile extends File { /** * Get the URL of the file description page. - * @return String + * @return string */ function getDescriptionUrl() { return $this->title->getLocalURL(); @@ -1686,7 +1766,7 @@ class LocalFile extends File { * This is not used by ImagePage for local files, since (among other things) * it skips the parser cache. * - * @param $lang Language What language to get description in (Optional) + * @param Language $lang What language to get description in (Optional) * @return bool|mixed */ function getDescriptionText( $lang = null ) { @@ -1699,10 +1779,13 @@ class LocalFile extends File { return false; } $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) ); + return $pout->getText(); } /** + * @param int $audience + * @param User $user * @return string */ function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) { @@ -1710,8 +1793,8 @@ class LocalFile extends File { if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) { return ''; } elseif ( $audience == self::FOR_THIS_USER - && !$this->userCan( self::DELETED_COMMENT, $user ) ) - { + && !$this->userCan( self::DELETED_COMMENT, $user ) + ) { return ''; } else { return $this->description; @@ -1723,6 +1806,7 @@ class LocalFile extends File { */ function getTimestamp() { $this->load(); + return $this->timestamp; } @@ -1756,15 +1840,17 @@ class LocalFile extends File { */ function isCacheable() { $this->load(); + // If extra data (metadata) was not loaded then it must have been large return $this->extraDataLoaded - && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN; + && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN; } /** * Start a transaction and lock the image for update * Increments a reference counter if the lock is already held - * @return boolean True if the image exists, false otherwise + * @throws MWException Throws an error if the lock was not acquired + * @return bool Success */ function lock() { $dbw = $this->repo->getMasterDB(); @@ -1776,19 +1862,22 @@ class LocalFile extends File { } $this->locked++; // Bug 54736: use simple lock to handle when the file does not exist. - // SELECT FOR UPDATE only locks records not the gaps where there are none. - $cache = wfGetMainCache(); - $key = $this->getCacheKey(); - if ( !$cache->lock( $key, 60 ) ) { + // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE. + // Also, that would cause contention on INSERT of similarly named rows. + $backend = $this->getRepo()->getBackend(); + $lockPaths = array( $this->getPath() ); // represents all versions of the file + $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 ); + if ( !$status->isGood() ) { throw new MWException( "Could not acquire lock for '{$this->getName()}.'" ); } - $dbw->onTransactionIdle( function() use ( $cache, $key ) { - $cache->unlock( $key ); // release on commit + $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) { + $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX ); // release on commit } ); } - return $dbw->selectField( 'image', '1', - array( 'img_name' => $this->getName() ), __METHOD__, array( 'FOR UPDATE' ) ); + $this->markVolatile(); // file may change soon + + return true; } /** @@ -1807,6 +1896,48 @@ class LocalFile extends File { } /** + * Mark a file as about to be changed + * + * This sets a cache key that alters master/slave DB loading behavior + * + * @return bool Success + */ + protected function markVolatile() { + global $wgMemc; + + $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) ); + if ( $key ) { + $this->lastMarkedVolatile = time(); + return $wgMemc->set( $key, $this->lastMarkedVolatile, self::VOLATILE_TTL ); + } + + return true; + } + + /** + * Check if a file is about to be changed or has been changed recently + * + * @see LocalFile::isVolatile() + * @return bool Whether the file is volatile + */ + protected function isVolatile() { + global $wgMemc; + + $key = $this->repo->getSharedCacheKey( 'file-volatile', md5( $this->getName() ) ); + if ( !$key ) { + // repo unavailable; bail. + return false; + } + + if ( $this->lastMarkedVolatile === 0 ) { + $this->lastMarkedVolatile = $wgMemc->get( $key ) ?: 0; + } + + $volatileDuration = time() - $this->lastMarkedVolatile; + return $volatileDuration <= self::VOLATILE_TTL; + } + + /** * Roll back the DB transaction and mark the image unlocked */ function unlockAndRollback() { @@ -1823,6 +1954,13 @@ class LocalFile extends File { return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(), $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() ); } + + /** + * Clean up any dangling locks + */ + function __destruct() { + $this->unlock(); + } } // LocalFile class # ------------------------------------------------------------------------------ @@ -1832,24 +1970,46 @@ class LocalFile extends File { * @ingroup FileAbstraction */ class LocalFileDeleteBatch { + /** @var LocalFile */ + private $file; - /** - * @var LocalFile - */ - var $file; + /** @var string */ + private $reason; + + /** @var array */ + private $srcRels = array(); + + /** @var array */ + private $archiveUrls = array(); + + /** @var array Items to be processed in the deletion batch */ + private $deletionBatch; - var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress; - var $status; + /** @var bool Wether to suppress all suppressable fields when deleting */ + private $suppress; + + /** @var FileRepoStatus */ + private $status; + + /** @var User */ + private $user; /** - * @param $file File - * @param $reason string - * @param $suppress bool + * @param File $file + * @param string $reason + * @param bool $suppress + * @param User|null $user */ - function __construct( File $file, $reason = '', $suppress = false ) { + function __construct( File $file, $reason = '', $suppress = false, $user = null ) { $this->file = $file; $this->reason = $reason; $this->suppress = $suppress; + if ( $user ) { + $this->user = $user; + } else { + global $wgUser; + $this->user = $wgUser; + } $this->status = $file->repo->newGood(); } @@ -1858,7 +2018,7 @@ class LocalFileDeleteBatch { } /** - * @param $oldName string + * @param string $oldName */ function addOld( $oldName ) { $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName ); @@ -1867,7 +2027,7 @@ class LocalFileDeleteBatch { /** * Add the old versions of the image to the batch - * @return Array List of archive names from old versions + * @return array List of archive names from old versions */ function addOlds() { $archiveNames = array(); @@ -1919,7 +2079,8 @@ class LocalFileDeleteBatch { $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ), - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')', + array( 'oi_archive_name' => array_keys( $oldRels ), + 'oi_name' => $this->file->getName() ), // performance __METHOD__ ); @@ -1962,11 +2123,9 @@ class LocalFileDeleteBatch { } function doDBInserts() { - global $wgUser; - $dbw = $this->file->repo->getMasterDB(); $encTimestamp = $dbw->addQuotes( $dbw->timestamp() ); - $encUserId = $dbw->addQuotes( $wgUser->getId() ); + $encUserId = $dbw->addQuotes( $this->user->getId() ); $encReason = $dbw->addQuotes( $this->reason ); $encGroup = $dbw->addQuotes( 'deleted' ); $ext = $this->file->getExtension(); @@ -1992,27 +2151,31 @@ class LocalFileDeleteBatch { $dbw->insertSelect( 'filearchive', 'image', array( 'fa_storage_group' => $encGroup, - 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END", - 'fa_deleted_user' => $encUserId, + 'fa_storage_key' => $dbw->conditional( + array( 'img_sha1' => '' ), + $dbw->addQuotes( '' ), + $concat + ), + 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, - 'fa_deleted_reason' => $encReason, - 'fa_deleted' => $this->suppress ? $bitfield : 0, + 'fa_deleted_reason' => $encReason, + 'fa_deleted' => $this->suppress ? $bitfield : 0, - 'fa_name' => 'img_name', + 'fa_name' => 'img_name', 'fa_archive_name' => 'NULL', - 'fa_size' => 'img_size', - 'fa_width' => 'img_width', - 'fa_height' => 'img_height', - 'fa_metadata' => 'img_metadata', - 'fa_bits' => 'img_bits', - 'fa_media_type' => 'img_media_type', - 'fa_major_mime' => 'img_major_mime', - 'fa_minor_mime' => 'img_minor_mime', - 'fa_description' => 'img_description', - 'fa_user' => 'img_user', - 'fa_user_text' => 'img_user_text', - 'fa_timestamp' => 'img_timestamp', - 'fa_sha1' => 'img_sha1', + 'fa_size' => 'img_size', + 'fa_width' => 'img_width', + 'fa_height' => 'img_height', + 'fa_metadata' => 'img_metadata', + 'fa_bits' => 'img_bits', + 'fa_media_type' => 'img_media_type', + 'fa_major_mime' => 'img_major_mime', + 'fa_minor_mime' => 'img_minor_mime', + 'fa_description' => 'img_description', + 'fa_user' => 'img_user', + 'fa_user_text' => 'img_user_text', + 'fa_timestamp' => 'img_timestamp', + 'fa_sha1' => 'img_sha1', ), $where, __METHOD__ ); } @@ -2020,31 +2183,35 @@ class LocalFileDeleteBatch { $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) ); $where = array( 'oi_name' => $this->file->getName(), - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ); + 'oi_archive_name' => array_keys( $oldRels ) ); $dbw->insertSelect( 'filearchive', 'oldimage', array( 'fa_storage_group' => $encGroup, - 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END", - 'fa_deleted_user' => $encUserId, + 'fa_storage_key' => $dbw->conditional( + array( 'oi_sha1' => '' ), + $dbw->addQuotes( '' ), + $concat + ), + 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, - 'fa_deleted_reason' => $encReason, - 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted', + 'fa_deleted_reason' => $encReason, + 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted', - 'fa_name' => 'oi_name', + 'fa_name' => 'oi_name', 'fa_archive_name' => 'oi_archive_name', - 'fa_size' => 'oi_size', - 'fa_width' => 'oi_width', - 'fa_height' => 'oi_height', - 'fa_metadata' => 'oi_metadata', - 'fa_bits' => 'oi_bits', - 'fa_media_type' => 'oi_media_type', - 'fa_major_mime' => 'oi_major_mime', - 'fa_minor_mime' => 'oi_minor_mime', - 'fa_description' => 'oi_description', - 'fa_user' => 'oi_user', - 'fa_user_text' => 'oi_user_text', - 'fa_timestamp' => 'oi_timestamp', - 'fa_sha1' => 'oi_sha1', + 'fa_size' => 'oi_size', + 'fa_width' => 'oi_width', + 'fa_height' => 'oi_height', + 'fa_metadata' => 'oi_metadata', + 'fa_bits' => 'oi_bits', + 'fa_media_type' => 'oi_media_type', + 'fa_major_mime' => 'oi_major_mime', + 'fa_minor_mime' => 'oi_minor_mime', + 'fa_description' => 'oi_description', + 'fa_user' => 'oi_user', + 'fa_user_text' => 'oi_user_text', + 'fa_timestamp' => 'oi_timestamp', + 'fa_sha1' => 'oi_sha1', ), $where, __METHOD__ ); } } @@ -2083,7 +2250,7 @@ class LocalFileDeleteBatch { $res = $dbw->select( 'oldimage', array( 'oi_archive_name' ), array( 'oi_name' => $this->file->getName(), - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')', + 'oi_archive_name' => array_keys( $oldRels ), $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ), __METHOD__ ); @@ -2117,7 +2284,12 @@ class LocalFileDeleteBatch { $this->doDBInserts(); // Removes non-existent file from the batch, so we don't get errors. - $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch ); + $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch ); + if ( !$checkStatus->isGood() ) { + $this->status->merge( $checkStatus ); + return $this->status; + } + $this->deletionBatch = $checkStatus->value; // Execute the file deletion batch $status = $this->file->repo->deleteBatch( $this->deletionBatch ); @@ -2132,6 +2304,7 @@ class LocalFileDeleteBatch { // TODO: delete the defunct filearchive rows if we are using a non-transactional DB $this->file->unlockAndRollback(); wfProfileOut( __METHOD__ ); + return $this->status; } @@ -2147,8 +2320,8 @@ class LocalFileDeleteBatch { /** * Removes non-existent files from a deletion batch. - * @param $batch array - * @return array + * @param array $batch + * @return Status */ function removeNonexistentFiles( $batch ) { $files = $newBatch = array(); @@ -2159,6 +2332,10 @@ class LocalFileDeleteBatch { } $result = $this->file->repo->fileExistsBatch( $files ); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } foreach ( $batch as $batchItem ) { if ( $result[$batchItem[0]] ) { @@ -2166,7 +2343,7 @@ class LocalFileDeleteBatch { } } - return $newBatch; + return Status::newGood( $newBatch ); } } @@ -2177,16 +2354,24 @@ class LocalFileDeleteBatch { * @ingroup FileAbstraction */ class LocalFileRestoreBatch { - /** - * @var LocalFile - */ - var $file; + /** @var LocalFile */ + private $file; + + /** @var array List of file IDs to restore */ + private $cleanupBatch; + + /** @var array List of file IDs to restore */ + private $ids; + + /** @var bool Add all revisions of the file */ + private $all; - var $cleanupBatch, $ids, $all, $unsuppress = false; + /** @var bool Wether to remove all settings for suppressed fields */ + private $unsuppress = false; /** - * @param $file File - * @param $unsuppress bool + * @param File $file + * @param bool $unsuppress */ function __construct( File $file, $unsuppress = false ) { $this->file = $file; @@ -2197,6 +2382,7 @@ class LocalFileRestoreBatch { /** * Add a file by ID + * @param int $fa_id */ function addId( $fa_id ) { $this->ids[] = $fa_id; @@ -2204,6 +2390,7 @@ class LocalFileRestoreBatch { /** * Add a whole lot of files by ID + * @param int[] $ids */ function addIds( $ids ) { $this->ids = array_merge( $this->ids, $ids ); @@ -2232,16 +2419,20 @@ class LocalFileRestoreBatch { return $this->file->repo->newGood(); } - $exists = $this->file->lock(); + $this->file->lock(); + $dbw = $this->file->repo->getMasterDB(); $status = $this->file->repo->newGood(); + $exists = (bool)$dbw->selectField( 'image', '1', + array( 'img_name' => $this->file->getName() ), __METHOD__, array( 'FOR UPDATE' ) ); + // Fetch all or selected archived revisions for the file, // sorted from the most recent to the oldest. $conditions = array( 'fa_name' => $this->file->getName() ); if ( !$this->all ) { - $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')'; + $conditions['fa_id'] = $this->ids; } $result = $dbw->select( @@ -2276,7 +2467,8 @@ class LocalFileRestoreBatch { continue; } - $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key; + $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . + $row->fa_storage_key; $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; if ( isset( $row->fa_sha1 ) ) { @@ -2294,7 +2486,8 @@ class LocalFileRestoreBatch { if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown' || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown' || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN' - || is_null( $row->fa_metadata ) ) { + || is_null( $row->fa_metadata ) + ) { // Refresh our metadata // Required for a new current revision; nice for older ones too. :) $props = RepoGroup::singleton()->getFileProps( $deletedUrl ); @@ -2303,7 +2496,7 @@ class LocalFileRestoreBatch { 'minor_mime' => $row->fa_minor_mime, 'major_mime' => $row->fa_major_mime, 'media_type' => $row->fa_media_type, - 'metadata' => $row->fa_metadata + 'metadata' => $row->fa_metadata ); } @@ -2311,20 +2504,20 @@ class LocalFileRestoreBatch { // This revision will be published as the new current version $destRel = $this->file->getRel(); $insertCurrent = array( - 'img_name' => $row->fa_name, - 'img_size' => $row->fa_size, - 'img_width' => $row->fa_width, - 'img_height' => $row->fa_height, - 'img_metadata' => $props['metadata'], - 'img_bits' => $row->fa_bits, - 'img_media_type' => $props['media_type'], - 'img_major_mime' => $props['major_mime'], - 'img_minor_mime' => $props['minor_mime'], + 'img_name' => $row->fa_name, + 'img_size' => $row->fa_size, + 'img_width' => $row->fa_width, + 'img_height' => $row->fa_height, + 'img_metadata' => $props['metadata'], + 'img_bits' => $row->fa_bits, + 'img_media_type' => $props['media_type'], + 'img_major_mime' => $props['major_mime'], + 'img_minor_mime' => $props['minor_mime'], 'img_description' => $row->fa_description, - 'img_user' => $row->fa_user, - 'img_user_text' => $row->fa_user_text, - 'img_timestamp' => $row->fa_timestamp, - 'img_sha1' => $sha1 + 'img_user' => $row->fa_user, + 'img_user_text' => $row->fa_user_text, + 'img_timestamp' => $row->fa_timestamp, + 'img_sha1' => $sha1 ); // The live (current) version cannot be hidden! @@ -2350,22 +2543,22 @@ class LocalFileRestoreBatch { $archiveNames[$archiveName] = true; $destRel = $this->file->getArchiveRel( $archiveName ); $insertBatch[] = array( - 'oi_name' => $row->fa_name, + 'oi_name' => $row->fa_name, 'oi_archive_name' => $archiveName, - 'oi_size' => $row->fa_size, - 'oi_width' => $row->fa_width, - 'oi_height' => $row->fa_height, - 'oi_bits' => $row->fa_bits, - 'oi_description' => $row->fa_description, - 'oi_user' => $row->fa_user, - 'oi_user_text' => $row->fa_user_text, - 'oi_timestamp' => $row->fa_timestamp, - 'oi_metadata' => $props['metadata'], - 'oi_media_type' => $props['media_type'], - 'oi_major_mime' => $props['major_mime'], - 'oi_minor_mime' => $props['minor_mime'], - 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted, - 'oi_sha1' => $sha1 ); + 'oi_size' => $row->fa_size, + 'oi_width' => $row->fa_width, + 'oi_height' => $row->fa_height, + 'oi_bits' => $row->fa_bits, + 'oi_description' => $row->fa_description, + 'oi_user' => $row->fa_user, + 'oi_user_text' => $row->fa_user_text, + 'oi_timestamp' => $row->fa_timestamp, + 'oi_metadata' => $props['metadata'], + 'oi_media_type' => $props['media_type'], + 'oi_major_mime' => $props['major_mime'], + 'oi_minor_mime' => $props['minor_mime'], + 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted, + 'oi_sha1' => $sha1 ); } $deleteIds[] = $row->fa_id; @@ -2391,7 +2584,12 @@ class LocalFileRestoreBatch { } // Remove missing files from batch, so we don't get errors when undeleting them - $storeBatch = $this->removeNonexistentFiles( $storeBatch ); + $checkStatus = $this->removeNonexistentFiles( $storeBatch ); + if ( !$checkStatus->isGood() ) { + $status->merge( $checkStatus ); + return $status; + } + $storeBatch = $checkStatus->value; // Run the store batch // Use the OVERWRITE_SAME flag to smooth over a common error @@ -2424,7 +2622,7 @@ class LocalFileRestoreBatch { if ( $deleteIds ) { $dbw->delete( 'filearchive', - array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), + array( 'fa_id' => $deleteIds ), __METHOD__ ); } @@ -2450,8 +2648,8 @@ class LocalFileRestoreBatch { /** * Removes non-existent files from a store batch. - * @param $triplets array - * @return array + * @param array $triplets + * @return Status */ function removeNonexistentFiles( $triplets ) { $files = $filteredTriplets = array(); @@ -2460,6 +2658,10 @@ class LocalFileRestoreBatch { } $result = $this->file->repo->fileExistsBatch( $files ); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } foreach ( $triplets as $file ) { if ( $result[$file[0]] ) { @@ -2467,12 +2669,12 @@ class LocalFileRestoreBatch { } } - return $filteredTriplets; + return Status::newGood( $filteredTriplets ); } /** * Removes non-existent files from a cleanup batch. - * @param $batch array + * @param array $batch * @return array */ function removeNonexistentFromCleanup( $batch ) { @@ -2541,23 +2743,22 @@ class LocalFileRestoreBatch { * @ingroup FileAbstraction */ class LocalFileMoveBatch { + /** @var LocalFile */ + protected $file; - /** - * @var LocalFile - */ - var $file; + /** @var Title */ + protected $target; - /** - * @var Title - */ - var $target; + protected $cur; - var $cur, $olds, $oldCount, $archive; + protected $olds; - /** - * @var DatabaseBase - */ - var $db; + protected $oldCount; + + protected $archive; + + /** @var DatabaseBase */ + protected $db; /** * @param File $file @@ -2584,7 +2785,7 @@ class LocalFileMoveBatch { /** * Add the old versions of the image to the batch - * @return Array List of archive names from old versions + * @return array List of archive names from old versions */ function addOlds() { $archiveBase = 'archive'; @@ -2595,7 +2796,8 @@ class LocalFileMoveBatch { $result = $this->db->select( 'oldimage', array( 'oi_archive_name', 'oi_deleted' ), array( 'oi_name' => $this->oldName ), - __METHOD__ + __METHOD__, + array( 'FOR UPDATE' ) // ignore snapshot ); foreach ( $result as $row ) { @@ -2640,9 +2842,16 @@ class LocalFileMoveBatch { $status = $repo->newGood(); $triplets = $this->getMoveTriplets(); - $triplets = $this->removeNonexistentFiles( $triplets ); + $checkStatus = $this->removeNonexistentFiles( $triplets ); + if ( !$checkStatus->isGood() ) { + $status->merge( $checkStatus ); + return $status; + } + $triplets = $checkStatus->value; + $destFile = wfLocalFile( $this->target ); $this->file->lock(); // begin + $destFile->lock(); // quickly fail if destination is not available // Rename the file versions metadata in the DB. // This implicitly locks the destination file, which avoids race conditions. // If we moved the files from A -> C before DB updates, another process could @@ -2650,25 +2859,32 @@ class LocalFileMoveBatch { // cleanupTarget() to trigger. It would delete the C files and cause data loss. $statusDb = $this->doDBUpdates(); if ( !$statusDb->isGood() ) { + $destFile->unlock(); $this->file->unlockAndRollback(); $statusDb->ok = false; + return $statusDb; } - wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); + wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " . + "{$statusDb->successCount} successes, {$statusDb->failCount} failures" ); // Copy the files into their new location. // If a prior process fataled copying or cleaning up files we tolerate any // of the existing files if they are identical to the ones being stored. $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME ); - wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" ); + wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " . + "{$statusMove->successCount} successes, {$statusMove->failCount} failures" ); if ( !$statusMove->isGood() ) { // Delete any files copied over (while the destination is still locked) $this->cleanupTarget( $triplets ); + $destFile->unlock(); $this->file->unlockAndRollback(); // unlocks the destination wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); $statusMove->ok = false; + return $statusMove; } + $destFile->unlock(); $this->file->unlock(); // done // Everything went ok, remove the source files @@ -2704,6 +2920,7 @@ class LocalFileMoveBatch { } else { $status->failCount++; $status->fatal( 'imageinvalidfilename' ); + return $status; } @@ -2745,7 +2962,10 @@ class LocalFileMoveBatch { // $move: (oldRelativePath, newRelativePath) $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] ); $triplets[] = array( $srcUrl, 'public', $move[1] ); - wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" ); + wfDebugLog( + 'imagemove', + "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}" + ); } return $triplets; @@ -2753,8 +2973,8 @@ class LocalFileMoveBatch { /** * Removes non-existent files from move batch. - * @param $triplets array - * @return array + * @param array $triplets + * @return Status */ function removeNonexistentFiles( $triplets ) { $files = array(); @@ -2764,8 +2984,12 @@ class LocalFileMoveBatch { } $result = $this->file->repo->fileExistsBatch( $files ); - $filteredTriplets = array(); + if ( in_array( null, $result, true ) ) { + return Status::newFatal( 'backend-fail-internal', + $this->file->repo->getBackend()->getName() ); + } + $filteredTriplets = array(); foreach ( $triplets as $file ) { if ( $result[$file[0]] ) { $filteredTriplets[] = $file; @@ -2774,12 +2998,13 @@ class LocalFileMoveBatch { } } - return $filteredTriplets; + return Status::newGood( $filteredTriplets ); } /** * Cleanup a partially moved array of triplets by deleting the target * files. Called if something went wrong half way. + * @param array $triplets */ function cleanupTarget( $triplets ) { // Create dest pairs from the triplets @@ -2795,6 +3020,7 @@ class LocalFileMoveBatch { /** * Cleanup a fully moved array of triplets by deleting the source files. * Called at the end of the move process if everything else went ok. + * @param array $triplets */ function cleanupSource( $triplets ) { // Create source file names from the triplets diff --git a/includes/filerepo/file/OldLocalFile.php b/includes/filerepo/file/OldLocalFile.php index 2c545963..710058fb 100644 --- a/includes/filerepo/file/OldLocalFile.php +++ b/includes/filerepo/file/OldLocalFile.php @@ -27,15 +27,19 @@ * @ingroup FileAbstraction */ class OldLocalFile extends LocalFile { - var $requestedTime, $archive_name; + /** @var string Timestamp */ + protected $requestedTime; + + /** @var string Archive name */ + protected $archive_name; const CACHE_VERSION = 1; const MAX_CACHE_ROWS = 20; /** - * @param $title Title - * @param $repo FileRepo - * @param $time null + * @param Title $title + * @param FileRepo $repo + * @param null|int $time Timestamp or null * @return OldLocalFile * @throws MWException */ @@ -44,13 +48,14 @@ class OldLocalFile extends LocalFile { if ( $time === null ) { throw new MWException( __METHOD__ . ' got null for $time parameter' ); } + return new self( $title, $repo, $time, null ); } /** - * @param $title Title - * @param $repo FileRepo - * @param $archiveName + * @param Title $title + * @param FileRepo $repo + * @param string $archiveName * @return OldLocalFile */ static function newFromArchiveName( $title, $repo, $archiveName ) { @@ -58,14 +63,15 @@ class OldLocalFile extends LocalFile { } /** - * @param $row - * @param $repo FileRepo + * @param stdClass $row + * @param FileRepo $repo * @return OldLocalFile */ static function newFromRow( $row, $repo ) { $title = Title::makeTitle( NS_FILE, $row->oi_name ); $file = new self( $title, $repo, null, $row->oi_archive_name ); $file->loadFromRow( $row, 'oi_' ); + return $file; } @@ -73,8 +79,8 @@ class OldLocalFile extends LocalFile { * Create a OldLocalFile from a SHA-1 key * Do not call this except from inside a repo class. * - * @param string $sha1 base-36 SHA-1 - * @param $repo LocalRepo + * @param string $sha1 Base-36 SHA-1 + * @param LocalRepo $repo * @param string|bool $timestamp MW_timestamp (optional) * * @return bool|OldLocalFile @@ -121,10 +127,10 @@ class OldLocalFile extends LocalFile { } /** - * @param $title Title - * @param $repo FileRepo - * @param string $time timestamp or null to load by archive name - * @param string $archiveName archive name or null to load by timestamp + * @param Title $title + * @param FileRepo $repo + * @param string $time Timestamp or null to load by archive name + * @param string $archiveName Archive name or null to load by timestamp * @throws MWException */ function __construct( $title, $repo, $time, $archiveName ) { @@ -144,12 +150,13 @@ class OldLocalFile extends LocalFile { } /** - * @return String + * @return string */ function getArchiveName() { if ( !isset( $this->archive_name ) ) { $this->load(); } + return $this->archive_name; } @@ -167,10 +174,11 @@ class OldLocalFile extends LocalFile { return $this->exists() && !$this->isDeleted( File::DELETED_FILE ); } - function loadFromDB() { + function loadFromDB( $flags = 0 ) { wfProfileIn( __METHOD__ ); $this->dataLoaded = true; + $dbr = $this->repo->getSlaveDB(); $conds = array( 'oi_name' => $this->getName() ); if ( is_null( $this->requestedTime ) ) { @@ -226,13 +234,14 @@ class OldLocalFile extends LocalFile { } /** - * @param $prefix string + * @param string $prefix * @return array */ function getCacheFields( $prefix = 'img_' ) { $fields = parent::getCacheFields( $prefix ); $fields[] = $prefix . 'archive_name'; $fields[] = $prefix . 'deleted'; + return $fields; } @@ -258,6 +267,7 @@ class OldLocalFile extends LocalFile { if ( !$this->fileExists ) { wfDebug( __METHOD__ . ": file does not exist, aborting\n" ); wfProfileOut( __METHOD__ ); + return; } @@ -267,15 +277,15 @@ class OldLocalFile extends LocalFile { wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . " to the current schema\n" ); $dbw->update( 'oldimage', array( - 'oi_size' => $this->size, // sanity - 'oi_width' => $this->width, - 'oi_height' => $this->height, - 'oi_bits' => $this->bits, + 'oi_size' => $this->size, // sanity + 'oi_width' => $this->width, + 'oi_height' => $this->height, + 'oi_bits' => $this->bits, 'oi_media_type' => $this->media_type, 'oi_major_mime' => $major, 'oi_minor_mime' => $minor, - 'oi_metadata' => $this->metadata, - 'oi_sha1' => $this->sha1, + 'oi_metadata' => $this->metadata, + 'oi_sha1' => $this->sha1, ), array( 'oi_name' => $this->getName(), 'oi_archive_name' => $this->archive_name ), @@ -285,12 +295,13 @@ class OldLocalFile extends LocalFile { } /** - * @param $field Integer: one of DELETED_* bitfield constants - * for file or revision rows + * @param int $field One of DELETED_* bitfield constants for file or + * revision rows * @return bool */ function isDeleted( $field ) { $this->load(); + return ( $this->deleted & $field ) == $field; } @@ -300,6 +311,7 @@ class OldLocalFile extends LocalFile { */ function getVisibility() { $this->load(); + return (int)$this->deleted; } @@ -307,12 +319,13 @@ class OldLocalFile extends LocalFile { * Determine if the current user is allowed to view a particular * field of this image file, if it's marked as deleted. * - * @param $field Integer - * @param $user User object to check, or null to use $wgUser + * @param int $field + * @param User|null $user User object to check, or null to use $wgUser * @return bool */ function userCan( $field, User $user = null ) { $this->load(); + return Revision::userCanBitfield( $this->deleted, $field, $user ); } @@ -321,12 +334,11 @@ class OldLocalFile extends LocalFile { * * @param string $srcPath File system path of the source file * @param string $archiveName Full archive name of the file, in the form - * $timestamp!$filename, where $filename must match $this->getName() - * - * @param $timestamp string - * @param $comment string - * @param $user - * @param $flags int + * $timestamp!$filename, where $filename must match $this->getName() + * @param string $timestamp + * @param string $comment + * @param User $user + * @param int $flags * @return FileRepoStatus */ function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) { @@ -353,9 +365,9 @@ class OldLocalFile extends LocalFile { * * @param string $srcPath File system path to the source file * @param string $archiveName The archive name of the file - * @param $timestamp string + * @param string $timestamp * @param string $comment Upload comment - * @param $user User User who did this upload + * @param User $user User who did this upload * @return bool */ function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) { @@ -370,21 +382,21 @@ class OldLocalFile extends LocalFile { $dbw->insert( 'oldimage', array( - 'oi_name' => $this->getName(), + 'oi_name' => $this->getName(), 'oi_archive_name' => $archiveName, - 'oi_size' => $props['size'], - 'oi_width' => intval( $props['width'] ), - 'oi_height' => intval( $props['height'] ), - 'oi_bits' => $props['bits'], - 'oi_timestamp' => $dbw->timestamp( $timestamp ), - 'oi_description' => $comment, - 'oi_user' => $user->getId(), - 'oi_user_text' => $user->getName(), - 'oi_metadata' => $props['metadata'], - 'oi_media_type' => $props['media_type'], - 'oi_major_mime' => $props['major_mime'], - 'oi_minor_mime' => $props['minor_mime'], - 'oi_sha1' => $props['sha1'], + 'oi_size' => $props['size'], + 'oi_width' => intval( $props['width'] ), + 'oi_height' => intval( $props['height'] ), + 'oi_bits' => $props['bits'], + 'oi_timestamp' => $dbw->timestamp( $timestamp ), + 'oi_description' => $comment, + 'oi_user' => $user->getId(), + 'oi_user_text' => $user->getName(), + 'oi_metadata' => $props['metadata'], + 'oi_media_type' => $props['media_type'], + 'oi_major_mime' => $props['major_mime'], + 'oi_minor_mime' => $props['minor_mime'], + 'oi_sha1' => $props['sha1'], ), __METHOD__ ); @@ -393,4 +405,17 @@ class OldLocalFile extends LocalFile { return true; } + /** + * If archive name is an empty string, then file does not "exist" + * + * This is the case for a couple files on Wikimedia servers where + * the old version is "lost". + */ + public function exists() { + $archiveName = $this->getArchiveName(); + if ( $archiveName === '' || !is_string( $archiveName ) ) { + return false; + } + return parent::exists(); + } } diff --git a/includes/filerepo/file/UnregisteredLocalFile.php b/includes/filerepo/file/UnregisteredLocalFile.php index 47ba6d6b..5a3e4e9c 100644 --- a/includes/filerepo/file/UnregisteredLocalFile.php +++ b/includes/filerepo/file/UnregisteredLocalFile.php @@ -27,23 +27,34 @@ * * Read-only. * - * TODO: Currently it doesn't really work in the repository role, there are + * @todo Currently it doesn't really work in the repository role, there are * lots of functions missing. It is used by the WebStore extension in the * standalone role. * * @ingroup FileAbstraction */ class UnregisteredLocalFile extends File { - var $title, $path, $mime, $dims, $metadata; + /** @var Title */ + protected $title; - /** - * @var MediaHandler - */ - var $handler; + /** @var string */ + protected $path; + + /** @var bool|string */ + protected $mime; + + /** @var array Dimension data */ + protected $dims; + + /** @var bool|string Handler-specific metadata which will be saved in the img_metadata field */ + protected $metadata; + + /** @var MediaHandler */ + public $handler; /** * @param string $path Storage path - * @param $mime string + * @param string $mime * @return UnregisteredLocalFile */ static function newFromPath( $path, $mime ) { @@ -51,8 +62,8 @@ class UnregisteredLocalFile extends File { } /** - * @param $title - * @param $repo + * @param Title $title + * @param FileRepo $repo * @return UnregisteredLocalFile */ static function newFromTitle( $title, $repo ) { @@ -64,14 +75,15 @@ class UnregisteredLocalFile extends File { * A FileRepo object is not required here, unlike most other File classes. * * @throws MWException - * @param $title Title|bool - * @param $repo FileRepo|bool - * @param $path string|bool - * @param $mime string|bool + * @param Title|bool $title + * @param FileRepo|bool $repo + * @param string|bool $path + * @param string|bool $mime */ function __construct( $title = false, $repo = false, $path = false, $mime = false ) { if ( !( $title && $repo ) && !$path ) { - throw new MWException( __METHOD__ . ': not enough parameters, must specify title and repo, or a full path' ); + throw new MWException( __METHOD__ . + ': not enough parameters, must specify title and repo, or a full path' ); } if ( $title instanceof Title ) { $this->title = File::normalizeTitle( $title, 'exception' ); @@ -95,7 +107,7 @@ class UnregisteredLocalFile extends File { } /** - * @param $page int + * @param int $page * @return bool */ private function cachePageDimensions( $page = 1 ) { @@ -105,24 +117,27 @@ class UnregisteredLocalFile extends File { } $this->dims[$page] = $this->handler->getPageDimensions( $this, $page ); } + return $this->dims[$page]; } /** - * @param $page int - * @return number + * @param int $page + * @return int */ function getWidth( $page = 1 ) { $dim = $this->cachePageDimensions( $page ); + return $dim['width']; } /** - * @param $page int - * @return number + * @param int $page + * @return int */ function getHeight( $page = 1 ) { $dim = $this->cachePageDimensions( $page ); + return $dim['height']; } @@ -134,17 +149,19 @@ class UnregisteredLocalFile extends File { $magic = MimeMagic::singleton(); $this->mime = $magic->guessMimeType( $this->getLocalRefPath() ); } + return $this->mime; } /** - * @param $filename String - * @return Array|bool + * @param string $filename + * @return array|bool */ function getImageSize( $filename ) { if ( !$this->getHandler() ) { return false; } + return $this->handler->getImageSize( $this, $this->getLocalRefPath() ); } @@ -159,6 +176,7 @@ class UnregisteredLocalFile extends File { $this->metadata = $this->handler->getMetadata( $this, $this->getLocalRefPath() ); } } + return $this->metadata; } @@ -179,6 +197,7 @@ class UnregisteredLocalFile extends File { */ function getSize() { $this->assertRepoDefined(); + return $this->repo->getFileSize( $this->path ); } @@ -187,7 +206,7 @@ class UnregisteredLocalFile extends File { * The file at the path of $fsFile should not be deleted (or at least * not until the end of the request). This is mostly a performance hack. * - * @param $fsFile FSFile + * @param FSFile $fsFile * @return void */ public function setLocalReference( FSFile $fsFile ) { |