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 | |
parent | b88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff) |
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/filerepo')
-rw-r--r-- | includes/filerepo/FSRepo.php | 6 | ||||
-rw-r--r-- | includes/filerepo/FileRepo.php | 453 | ||||
-rw-r--r-- | includes/filerepo/FileRepoStatus.php | 11 | ||||
-rw-r--r-- | includes/filerepo/ForeignAPIRepo.php | 139 | ||||
-rw-r--r-- | includes/filerepo/ForeignDBRepo.php | 46 | ||||
-rw-r--r-- | includes/filerepo/ForeignDBViaLBRepo.php | 24 | ||||
-rw-r--r-- | includes/filerepo/LocalRepo.php | 195 | ||||
-rw-r--r-- | includes/filerepo/NullRepo.php | 6 | ||||
-rw-r--r-- | includes/filerepo/RepoGroup.php | 186 | ||||
-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 |
16 files changed, 2215 insertions, 1136 deletions
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 42c9c945..5896aba1 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -31,9 +31,8 @@ * @deprecated since 1.19 */ class FSRepo extends FileRepo { - /** - * @param $info array + * @param array $info * @throws MWException */ function __construct( array $info ) { @@ -57,7 +56,8 @@ class FSRepo extends FileRepo { // Get the FS backend configuration $backend = new FSFileBackend( array( 'name' => $info['name'] . '-backend', - 'lockManager' => 'fsLockManager', + 'wikiId' => wfWikiID(), + 'lockManager' => LockManagerGroup::singleton( wfWikiID() )->get( 'fsLockManager' ), 'containerPaths' => array( "{$repoName}-public" => "{$directory}", "{$repoName}-temp" => "{$directory}/temp", diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 1195d5f8..59295257 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -40,29 +40,91 @@ class FileRepo { const OVERWRITE_SAME = 4; const SKIP_LOCKING = 8; + const NAME_AND_TIME_ONLY = 1; + + /** @var bool Whether to fetch commons image description pages and display + * them on the local wiki */ + public $fetchDescription; + + /** @var int */ + public $descriptionCacheExpiry; + /** @var FileBackend */ protected $backend; - /** @var Array Map of zones to config */ + + /** @var array Map of zones to config */ protected $zones = array(); - var $thumbScriptUrl, $transformVia404; - var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; - var $fetchDescription, $initialCapital; - var $pathDisclosureProtection = 'simple'; // 'paranoid' - var $descriptionCacheExpiry, $url, $thumbUrl; - var $hashLevels, $deletedHashLevels; + /** @var string URL of thumb.php */ + protected $thumbScriptUrl; + + /** @var bool Whether to skip media file transformation on parse and rely + * on a 404 handler instead. */ + protected $transformVia404; + + /** @var string URL of image description pages, e.g. + * http://en.wikipedia.org/wiki/File: + */ + protected $descBaseUrl; + + /** @var string URL of the MediaWiki installation, equivalent to + * $wgScriptPath, e.g. https://en.wikipedia.org/w + */ + protected $scriptDirUrl; + + /** @var string Script extension of the MediaWiki installation, equivalent + * to $wgScriptExtension, e.g. .php5 defaults to .php */ + protected $scriptExtension; + + /** @var string Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 */ + protected $articleUrl; + + /** @var bool Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], + * determines whether filenames implicitly start with a capital letter. + * The current implementation may give incorrect description page links + * when the local $wgCapitalLinks and initialCapital are mismatched. + */ + protected $initialCapital; + + /** @var string May be 'paranoid' to remove all parameters from error + * messages, 'none' to leave the paths in unchanged, or 'simple' to + * replace paths with placeholders. Default for LocalRepo is + * 'simple'. + */ + protected $pathDisclosureProtection = 'simple'; + + /** @var bool Public zone URL. */ + protected $url; + + /** @var string The base thumbnail URL. Defaults to "<url>/thumb". */ + protected $thumbUrl; + + /** @var int The number of directory levels for hash-based division of files */ + protected $hashLevels; + + /** @var int The number of directory levels for hash-based division of deleted files */ + protected $deletedHashLevels; + + /** @var int File names over this size will use the short form of thumbnail + * names. Short thumbnail names only have the width, parameters, and the + * extension. + */ protected $abbrvThreshold; + /** @var string The URL of the repo's favicon, if any */ + protected $favicon; + /** * Factory functions for creating new files * Override these in the base class */ - var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); - var $oldFileFactory = false; - var $fileFactoryKey = false, $oldFileFactoryKey = false; + protected $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); + protected $oldFileFactory = false; + protected $fileFactoryKey = false; + protected $oldFileFactoryKey = false; /** - * @param $info array|null + * @param array|null $info * @throws MWException */ public function __construct( array $info = null ) { @@ -72,7 +134,8 @@ class FileRepo { || !array_key_exists( 'name', $info ) || !array_key_exists( 'backend', $info ) ) { - throw new MWException( __CLASS__ . " requires an array of options having both 'name' and 'backend' keys.\n" ); + throw new MWException( __CLASS__ . + " requires an array of options having both 'name' and 'backend' keys.\n" ); } // Required settings @@ -87,7 +150,7 @@ class FileRepo { $optionalSettings = array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry', - 'scriptExtension' + 'scriptExtension', 'favicon' ); foreach ( $optionalSettings as $var ) { if ( isset( $info[$var] ) ) { @@ -167,13 +230,14 @@ class FileRepo { throw new MWException( "No '$zone' zone defined in the {$this->name} repo." ); } } + return $status; } /** * Determine if a string is an mwrepo:// URL * - * @param $url string + * @param string $url * @return bool */ public static function isVirtualUrl( $url ) { @@ -185,7 +249,7 @@ class FileRepo { * The suffix, if supplied, is considered to be unencoded, and will be * URL-encoded before being returned. * - * @param $suffix string|bool + * @param string|bool $suffix * @return string */ public function getVirtualUrl( $suffix = false ) { @@ -193,6 +257,7 @@ class FileRepo { if ( $suffix !== false ) { $path .= '/' . rawurlencode( $suffix ); } + return $path; } @@ -201,14 +266,17 @@ class FileRepo { * * @param string $zone One of: public, deleted, temp, thumb * @param string|null $ext Optional file extension - * @return String or false + * @return string|bool */ public function getZoneUrl( $zone, $ext = null ) { - if ( in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) { // standard public zones + if ( in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) { + // standard public zones if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) { - return $this->zones[$zone]['urlsByExt'][$ext]; // custom URL for extension/zone + // custom URL for extension/zone + return $this->zones[$zone]['urlsByExt'][$ext]; } elseif ( isset( $this->zones[$zone]['url'] ) ) { - return $this->zones[$zone]['url']; // custom URL for zone + // custom URL for zone + return $this->zones[$zone]['url']; } } switch ( $zone ) { @@ -228,32 +296,17 @@ class FileRepo { } /** - * Get the thumb zone URL configured to be handled by scripts like thumb_handler.php. - * This is probably only useful for internal requests, such as from a fast frontend server - * to a slower backend server. - * - * Large sites may use a different host name for uploads than for wikis. In any case, the - * wiki configuration is needed in order to use thumb.php. To avoid extracting the wiki ID - * from the URL path, one can configure thumb_handler.php to recognize a special path on the - * same host name as the wiki that is used for viewing thumbnails. - * - * @param string $zone one of: public, deleted, temp, thumb - * @return String or false + * @return bool Whether non-ASCII path characters are allowed */ - public function getZoneHandlerUrl( $zone ) { - if ( isset( $this->zones[$zone]['handlerUrl'] ) - && in_array( $zone, array( 'public', 'temp', 'thumb', 'transcoded' ) ) ) - { - return $this->zones[$zone]['handlerUrl']; - } - return false; + public function backendSupportsUnicodePaths() { + return ( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS ); } /** * Get the backend storage path corresponding to a virtual URL. * Use this function wisely. * - * @param $url string + * @param string $url * @throws MWException * @return string */ @@ -273,26 +326,28 @@ class FileRepo { if ( !$base ) { throw new MWException( __METHOD__ . ": invalid zone: $zone" ); } + return $base . '/' . rawurldecode( $rel ); } /** * The the storage container and base path of a zone * - * @param $zone string - * @return Array (container, base path) or (null, null) + * @param string $zone + * @return array (container, base path) or (null, null) */ protected function getZoneLocation( $zone ) { if ( !isset( $this->zones[$zone] ) ) { return array( null, null ); // bogus } + return array( $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ); } /** * Get the storage path corresponding to one of the zones * - * @param $zone string + * @param string $zone * @return string|null Returns null if the zone is not defined */ public function getZonePath( $zone ) { @@ -304,18 +359,19 @@ class FileRepo { if ( $base != '' ) { // may not be set $base = "/{$base}"; } + return "mwstore://$backendName/{$container}{$base}"; } /** * Create a new File object from the local repository * - * @param $title Mixed: Title object or string - * @param $time Mixed: Time at which the image was uploaded. - * If this is specified, the returned object will be an - * instance of the repository's old file class instead of a - * current file. Repositories not supporting version control - * should return false if this parameter is set. + * @param Title|string $title Title object or string + * @param bool|string $time Time at which the image was uploaded. If this + * is specified, the returned object will be an instance of the + * repository's old file class instead of a current file. Repositories + * not supporting version control should return false if this parameter + * is set. * @return File|null A File, or null if passed an invalid Title */ public function newFile( $title, $time = false ) { @@ -339,17 +395,15 @@ class FileRepo { * Returns false if the file does not exist. Repositories not supporting * version control should return false if the time is specified. * - * @param $title Mixed: Title object or string + * @param Title|string $title Title object or string * @param array $options Associative array of options: - * time: requested time for a specific file version, or false for the - * current version. An image object will be returned which was - * created at the specified time (which may be archived or current). - * - * ignoreRedirect: If true, do not follow file redirects - * - * private: If true, return restricted (deleted) files if the current - * user is allowed to view them. Otherwise, such files will not - * be found. + * time: requested time for a specific file version, or false for the + * current version. An image object will be returned which was + * created at the specified time (which may be archived or current). + * ignoreRedirect: If true, do not follow file redirects + * private: If true, return restricted (deleted) files if the current + * user is allowed to view them. Otherwise, such files will not + * be found. If a User object, use that user instead of the current. * @return File|bool False on failure */ public function findFile( $title, $options = array() ) { @@ -372,7 +426,11 @@ class FileRepo { if ( $img && $img->exists() ) { if ( !$img->isDeleted( File::DELETED_FILE ) ) { return $img; // always OK - } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { + } elseif ( !empty( $options['private'] ) && + $img->userCan( File::DELETED_FILE, + $options['private'] instanceof User ? $options['private'] : null + ) + ) { return $img; } } @@ -390,9 +448,11 @@ class FileRepo { } if ( $img->exists() ) { $img->redirectedFrom( $title->getDBkey() ); + return $img; } } + return false; } @@ -405,9 +465,15 @@ class FileRepo { * $findItem = array( 'title' => $title, 'private' => true ); * $findBatch = array( $findItem ); * $repo->findFiles( $findBatch ); - * @return array + * + * No title should appear in $items twice, as the result use titles as keys + * @param int $flags Supports: + * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map. + * The search title uses the input titles; the other is the final post-redirect title. + * All titles are returned as string DB keys and the inner array is associative. + * @return array Map of (file name => File objects) for matches */ - public function findFiles( array $items ) { + public function findFiles( array $items, $flags = 0 ) { $result = array(); foreach ( $items as $item ) { if ( is_array( $item ) ) { @@ -420,9 +486,18 @@ class FileRepo { } $file = $this->findFile( $title, $options ); if ( $file ) { - $result[$file->getTitle()->getDBkey()] = $file; + $searchName = File::normalizeTitle( $title )->getDBkey(); // must be valid + if ( $flags & self::NAME_AND_TIME_ONLY ) { + $result[$searchName] = array( + 'title' => $file->getTitle()->getDBkey(), + 'timestamp' => $file->getTimestamp() + ); + } else { + $result[$searchName] = $file; + } } } + return $result; } @@ -431,7 +506,7 @@ class FileRepo { * Returns false if the file does not exist. Repositories not supporting * version control should return false if the time is specified. * - * @param string $sha1 base 36 SHA-1 hash + * @param string $sha1 Base 36 SHA-1 hash * @param array $options Option array, same as findFile(). * @return File|bool False on failure */ @@ -452,11 +527,16 @@ class FileRepo { if ( $img && $img->exists() ) { if ( !$img->isDeleted( File::DELETED_FILE ) ) { return $img; // always OK - } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { + } elseif ( !empty( $options['private'] ) && + $img->userCan( File::DELETED_FILE, + $options['private'] instanceof User ? $options['private'] : null + ) + ) { return $img; } } } + return false; } @@ -465,8 +545,8 @@ class FileRepo { * SHA-1 content hash. * * STUB - * @param $hash - * @return array + * @param string $hash SHA-1 hash + * @return File[] */ public function findBySha1( $hash ) { return array(); @@ -487,6 +567,7 @@ class FileRepo { $result[$hash] = $files; } } + return $result; } @@ -531,10 +612,10 @@ class FileRepo { } /** - * Get the name of an image from its title object + * Get the name of a file from its title object * - * @param $title Title - * @return String + * @param Title $title + * @return string */ public function getNameFromTitle( Title $title ) { global $wgContLang; @@ -546,6 +627,7 @@ class FileRepo { } else { $name = $title->getDBkey(); } + return $name; } @@ -583,8 +665,8 @@ class FileRepo { } /** - * @param $name - * @param $levels + * @param string $name + * @param int $levels * @return string */ protected static function getHashPathForLevel( $name, $levels ) { @@ -596,6 +678,7 @@ class FileRepo { for ( $i = 1; $i <= $levels; $i++ ) { $path .= substr( $hash, 0, $i ) . '/'; } + return $path; } } @@ -603,7 +686,7 @@ class FileRepo { /** * Get the number of hash directory levels * - * @return integer + * @return int */ public function getHashLevels() { return $this->hashLevels; @@ -621,15 +704,17 @@ class FileRepo { /** * Make an url to this repo * - * @param $query mixed Query string to append + * @param string $query Query string to append * @param string $entry Entry point; defaults to index * @return string|bool False on failure */ public function makeUrl( $query = '', $entry = 'index' ) { if ( isset( $this->scriptDirUrl ) ) { $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; + return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); } + return false; } @@ -642,13 +727,13 @@ class FileRepo { * In particular, it uses the article paths as specified to the repository * constructor, whereas local repositories use the local Title functions. * - * @param $name string + * @param string $name * @return string */ public function getDescriptionUrl( $name ) { $encName = wfUrlencode( $name ); if ( !is_null( $this->descBaseUrl ) ) { - # "http://example.com/wiki/Image:" + # "http://example.com/wiki/File:" return $this->descBaseUrl . $encName; } if ( !is_null( $this->articleUrl ) ) { @@ -667,6 +752,7 @@ class FileRepo { # and just sort of hope index.php is right. ;) return $this->makeUrl( "title=Image:$encName" ); } + return false; } @@ -676,8 +762,8 @@ class FileRepo { * repository's file class, since it may return invalid results. User code * should use File::getDescriptionText(). * - * @param string $name name of image to fetch - * @param string $lang language to fetch it in, if any. + * @param string $name Name of image to fetch + * @param string $lang Language to fetch it in, if any. * @return string */ public function getDescriptionRenderUrl( $name, $lang = null ) { @@ -710,21 +796,22 @@ class FileRepo { return $this->makeUrl( 'title=MediaWiki:Filepage.css&' . wfArrayToCgi( Skin::getDynamicStylesheetQuery() ) ); } + return false; } /** * Store a file to a given destination. * - * @param string $srcPath source file system path, storage path, or virtual URL - * @param string $dstZone destination zone - * @param string $dstRel destination relative path - * @param $flags Integer: bitwise combination of the following flags: - * self::DELETE_SOURCE Delete the source file after upload - * self::OVERWRITE Overwrite an existing destination file instead of failing - * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the - * same contents as the source - * self::SKIP_LOCKING Skip any file locking when doing the store + * @param string $srcPath Source file system path, storage path, or virtual URL + * @param string $dstZone Destination zone + * @param string $dstRel Destination relative path + * @param int $flags Bitwise combination of the following flags: + * self::DELETE_SOURCE Delete the source file after upload + * self::OVERWRITE Overwrite an existing destination file instead of failing + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the + * same contents as the source + * self::SKIP_LOCKING Skip any file locking when doing the store * @return FileRepoStatus */ public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { @@ -742,12 +829,12 @@ class FileRepo { * Store a batch of files * * @param array $triplets (src, dest zone, dest rel) triplets as per store() - * @param $flags Integer: bitwise combination of the following flags: - * self::DELETE_SOURCE Delete the source file after upload - * self::OVERWRITE Overwrite an existing destination file instead of failing - * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the - * same contents as the source - * self::SKIP_LOCKING Skip any file locking when doing the store + * @param int $flags Bitwise combination of the following flags: + * self::DELETE_SOURCE Delete the source file after upload + * self::OVERWRITE Overwrite an existing destination file instead of failing + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the + * same contents as the source + * self::SKIP_LOCKING Skip any file locking when doing the store * @throws MWException * @return FileRepoStatus */ @@ -824,8 +911,8 @@ class FileRepo { * It will try to delete each file, but ignores any errors that may occur. * * @param array $files List of files to delete - * @param $flags Integer: bitwise combination of the following flags: - * self::SKIP_LOCKING Skip any file locking when doing the deletions + * @param int $flags Bitwise combination of the following flags: + * self::SKIP_LOCKING Skip any file locking when doing the deletions * @return FileRepoStatus */ public function cleanupBatch( array $files, $flags = 0 ) { @@ -863,11 +950,13 @@ class FileRepo { * * @param string $src Source file system path, storage path, or virtual URL * @param string $dst Virtual URL or storage path - * @param string|null $disposition Content-Disposition if given and supported + * @param array|string|null $options An array consisting of a key named headers + * listing extra headers. If a string, taken as content-disposition header. + * (Support for array of options new in 1.23) * @return FileRepoStatus */ - final public function quickImport( $src, $dst, $disposition = null ) { - return $this->quickImportBatch( array( array( $src, $dst, $disposition ) ) ); + final public function quickImport( $src, $dst, $options = null ) { + return $this->quickImportBatch( array( array( $src, $dst, $options ) ) ); } /** @@ -904,7 +993,7 @@ class FileRepo { * This is intended for copying generated thumbnails into the repo. * * All path parameters may be a file system path, storage path, or virtual URL. - * When "dispositions" are given they are used as Content-Disposition if supported. + * When "headers" are given they are used as HTTP headers if supported. * * @param array $triples List of (source path, destination path, disposition) * @return FileRepoStatus @@ -916,11 +1005,21 @@ class FileRepo { list( $src, $dst ) = $triple; $src = $this->resolveToStoragePath( $src ); $dst = $this->resolveToStoragePath( $dst ); + + if ( !isset( $triple[2] ) ) { + $headers = array(); + } elseif ( is_string( $triple[2] ) ) { + // back-compat + $headers = array( 'Content-Disposition' => $triple[2] ); + } elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) { + $headers = $triple[2]['headers']; + } + // @fixme: $headers might not be defined $operations[] = array( 'op' => FileBackend::isStoragePath( $src ) ? 'copy' : 'store', 'src' => $src, 'dst' => $dst, - 'disposition' => isset( $triple[2] ) ? $triple[2] : null + 'headers' => $headers ); $status->merge( $this->initDirectory( dirname( $dst ) ) ); } @@ -957,10 +1056,10 @@ class FileRepo { * Returns a FileRepoStatus object with the file Virtual URL in the value, * file can later be disposed using FileRepo::freeTemp(). * - * @param string $originalName the base name of the file as specified - * by the user. The file extension will be maintained. - * @param string $srcPath the current location of the file. - * @return FileRepoStatus object with the URL in the value. + * @param string $originalName The base name of the file as specified + * by the user. The file extension will be maintained. + * @param string $srcPath The current location of the file. + * @return FileRepoStatus Object with the URL in the value. */ public function storeTemp( $originalName, $srcPath ) { $this->assertWritableRepo(); // fail out if read-only @@ -979,8 +1078,8 @@ class FileRepo { /** * Remove a temporary file or mark it for garbage collection * - * @param string $virtualUrl the virtual URL returned by FileRepo::storeTemp() - * @return Boolean: true on success, false on failure + * @param string $virtualUrl The virtual URL returned by FileRepo::storeTemp() + * @return bool True on success, false on failure */ public function freeTemp( $virtualUrl ) { $this->assertWritableRepo(); // fail out if read-only @@ -988,6 +1087,7 @@ class FileRepo { $temp = $this->getVirtualUrl( 'temp' ); if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) { wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" ); + return false; } @@ -999,8 +1099,8 @@ class FileRepo { * * @param array $srcPaths Ordered list of source virtual URLs/storage paths * @param string $dstPath Target file system path - * @param $flags Integer: bitwise combination of the following flags: - * self::DELETE_SOURCE Delete the source files + * @param int $flags Bitwise combination of the following flags: + * self::DELETE_SOURCE Delete the source files * @return FileRepoStatus */ public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) { @@ -1043,12 +1143,12 @@ class FileRepo { * Options to $options include: * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests * - * @param string $srcPath the source file system path, storage path, or URL - * @param string $dstRel the destination relative path - * @param string $archiveRel the relative path where the existing file is to - * be archived, if there is one. Relative to the public zone root. - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate - * that the source file should be deleted if possible + * @param string $srcPath The source file system path, storage path, or URL + * @param string $dstRel The destination relative path + * @param string $archiveRel The relative path where the existing file is to + * be archived, if there is one. Relative to the public zone root. + * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source file should be deleted if possible * @param array $options Optional additional parameters * @return FileRepoStatus */ @@ -1075,9 +1175,9 @@ class FileRepo { * Publish a batch of files * * @param array $ntuples (source, dest, archive) triplets or - * (source, dest, archive, options) 4-tuples as per publish(). - * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate - * that the source files should be deleted if possible + * (source, dest, archive, options) 4-tuples as per publish(). + * @param int $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * that the source files should be deleted if possible * @throws MWException * @return FileRepoStatus */ @@ -1129,7 +1229,7 @@ class FileRepo { // This will check if the archive file also exists and fail if does. // This is a sanity check to avoid data loss. On Windows and Linux, // copy() will overwrite, so the existence check is vulnerable to - // race conditions unless an functioning LockManager is used. + // race conditions unless a functioning LockManager is used. // LocalFile also uses SELECT FOR UPDATE for synchronization. $operations[] = array( 'op' => 'copy', @@ -1238,6 +1338,7 @@ class FileRepo { */ public function fileExists( $file ) { $result = $this->fileExistsBatch( array( $file ) ); + return $result[0]; } @@ -1245,14 +1346,18 @@ class FileRepo { * Checks existence of an array of files. * * @param array $files Virtual URLs (or storage paths) of files to check - * @return array|bool Either array of files and existence flags, or false + * @return array Map of files and existence flags, or false */ public function fileExistsBatch( array $files ) { + $paths = array_map( array( $this, 'resolveToStoragePath' ), $files ); + $this->backend->preloadFileStat( array( 'srcs' => $paths ) ); + $result = array(); foreach ( $files as $key => $file ) { - $file = $this->resolveToStoragePath( $file ); - $result[$key] = $this->backend->fileExists( array( 'src' => $file ) ); + $path = $this->resolveToStoragePath( $file ); + $result[$key] = $this->backend->fileExists( array( 'src' => $path ) ); } + return $result; } @@ -1261,10 +1366,10 @@ class FileRepo { * If no valid deletion archive exists, this may either delete the file * or throw an exception, depending on the preference of the repository * - * @param $srcRel Mixed: relative path for the file to be deleted - * @param $archiveRel Mixed: relative path for the archive location. - * Relative to a private archive directory. - * @return FileRepoStatus object + * @param mixed $srcRel Relative path for the file to be deleted + * @param mixed $archiveRel Relative path for the archive location. + * Relative to a private archive directory. + * @return FileRepoStatus */ public function delete( $srcRel, $archiveRel ) { $this->assertWritableRepo(); // fail out if read-only @@ -1282,10 +1387,10 @@ class FileRepo { * assumes a naming scheme in the deleted zone based on content hash, as * opposed to the public zone which is assumed to be unique. * - * @param array $sourceDestPairs of source/destination pairs. Each element - * is a two-element array containing the source file path relative to the - * public root in the first element, and the archive file path relative - * to the deleted zone root in the second element. + * @param array $sourceDestPairs Array of source/destination pairs. Each element + * is a two-element array containing the source file path relative to the + * public root in the first element, and the archive file path relative + * to the deleted zone root in the second element. * @throws MWException * @return FileRepoStatus */ @@ -1346,6 +1451,7 @@ class FileRepo { * Delete files in the deleted directory if they are not referenced in the filearchive table * * STUB + * @param array $storageKeys */ public function cleanupDeletedBatch( array $storageKeys ) { $this->assertWritableRepo(); @@ -1355,7 +1461,7 @@ class FileRepo { * Get a relative path for a deletion archive key, * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg * - * @param $key string + * @param string $key * @throws MWException * @return string */ @@ -1367,6 +1473,7 @@ class FileRepo { for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) { $path .= $key[$i] . '/'; } + return $path; } @@ -1374,7 +1481,7 @@ class FileRepo { * If a path is a virtual URL, resolve it to a storage path. * Otherwise, just return the path as it is. * - * @param $path string + * @param string $path * @return string * @throws MWException */ @@ -1382,6 +1489,7 @@ class FileRepo { if ( $this->isVirtualUrl( $path ) ) { return $this->resolveVirtualUrl( $path ); } + return $path; } @@ -1389,11 +1497,12 @@ class FileRepo { * Get a local FS copy of a file with a given virtual URL/storage path. * Temporary files may be purged when the file object falls out of scope. * - * @param $virtualUrl string + * @param string $virtualUrl * @return TempFSFile|null Returns null on failure */ public function getLocalCopy( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getLocalCopy( array( 'src' => $path ) ); } @@ -1402,11 +1511,12 @@ class FileRepo { * The file is either an original or a copy. It should not be changed. * Temporary files may be purged when the file object falls out of scope. * - * @param $virtualUrl string + * @param string $virtualUrl * @return FSFile|null Returns null on failure. */ public function getLocalReference( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getLocalReference( array( 'src' => $path ) ); } @@ -1414,57 +1524,62 @@ class FileRepo { * Get properties of a file with a given virtual URL/storage path. * Properties should ultimately be obtained via FSFile::getProps(). * - * @param $virtualUrl string - * @return Array + * @param string $virtualUrl + * @return array */ public function getFileProps( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getFileProps( array( 'src' => $path ) ); } /** * Get the timestamp of a file with a given virtual URL/storage path * - * @param $virtualUrl string + * @param string $virtualUrl * @return string|bool False on failure */ public function getFileTimestamp( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getFileTimestamp( array( 'src' => $path ) ); } /** * Get the size of a file with a given virtual URL/storage path * - * @param $virtualUrl string - * @return integer|bool False on failure + * @param string $virtualUrl + * @return int|bool False on failure */ public function getFileSize( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getFileSize( array( 'src' => $path ) ); } /** * Get the sha1 (base 36) of a file with a given virtual URL/storage path * - * @param $virtualUrl string + * @param string $virtualUrl * @return string|bool */ public function getFileSha1( $virtualUrl ) { $path = $this->resolveToStoragePath( $virtualUrl ); + return $this->backend->getFileSha1Base36( array( 'src' => $path ) ); } /** * Attempt to stream a file with the given virtual URL/storage path * - * @param $virtualUrl string + * @param string $virtualUrl * @param array $headers Additional HTTP headers to send on success * @return bool Success */ public function streamFile( $virtualUrl, $headers = array() ) { $path = $this->resolveToStoragePath( $virtualUrl ); $params = array( 'src' => $path, 'headers' => $headers ); + return $this->backend->streamFile( $params )->isOK(); } @@ -1473,7 +1588,7 @@ class FileRepo { * This only acts on the current version of files, not any old versions. * May use either the database or the filesystem. * - * @param $callback Array|string + * @param callable $callback * @return void */ public function enumFiles( $callback ) { @@ -1484,7 +1599,7 @@ class FileRepo { * Call a callback function for every public file in the repository. * May use either the database or the filesystem. * - * @param $callback Array|string + * @param callable $callback * @return void */ protected function enumFilesInStorage( $callback ) { @@ -1509,20 +1624,21 @@ class FileRepo { /** * Determine if a relative path is valid, i.e. not blank or involving directory traveral * - * @param $filename string + * @param string $filename * @return bool */ public function validateFilename( $filename ) { if ( strval( $filename ) == '' ) { return false; } + return FileBackend::isPathTraversalFree( $filename ); } /** * Get a callback function to use for cleaning error message parameters * - * @return Array + * @return array */ function getErrorCleanupFunction() { switch ( $this->pathDisclosureProtection ) { @@ -1539,7 +1655,7 @@ class FileRepo { /** * Path disclosure protection function * - * @param $param string + * @param string $param * @return string */ function paranoidClean( $param ) { @@ -1549,7 +1665,7 @@ class FileRepo { /** * Path disclosure protection function * - * @param $param string + * @param string $param * @return string */ function passThrough( $param ) { @@ -1559,18 +1675,20 @@ class FileRepo { /** * Create a new fatal error * + * @param string $message * @return FileRepoStatus */ public function newFatal( $message /*, parameters...*/ ) { $params = func_get_args(); array_unshift( $params, $this ); + return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params ); } /** * Create a new good result * - * @param $value null|string + * @param null|string $value * @return FileRepoStatus */ public function newGood( $value = null ) { @@ -1582,8 +1700,8 @@ class FileRepo { * title object. If not, return false. * STUB * - * @param $title Title of image - * @return Bool + * @param Title $title Title of image + * @return bool */ public function checkRedirect( Title $title ) { return false; @@ -1594,9 +1712,10 @@ class FileRepo { * Doesn't do anything for repositories that don't support image redirects. * * STUB - * @param $title Title of image + * @param Title $title Title of image */ - public function invalidateImageRedirect( Title $title ) {} + public function invalidateImageRedirect( Title $title ) { + } /** * Get the human-readable name of the repo @@ -1604,10 +1723,12 @@ class FileRepo { * @return string */ public function getDisplayName() { - // We don't name our own repo, return nothing + global $wgSitename; + if ( $this->isLocal() ) { - return null; + return $wgSitename; } + // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); } @@ -1616,7 +1737,7 @@ class FileRepo { * Get the portion of the file that contains the origin file name. * If that name is too long, then the name "thumbnail.<ext>" will be given. * - * @param $name string + * @param string $name * @return string */ public function nameForThumb( $name ) { @@ -1624,6 +1745,7 @@ class FileRepo { $ext = FileBackend::extensionFromPath( $name ); $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext"; } + return $name; } @@ -1658,6 +1780,7 @@ class FileRepo { public function getLocalCacheKey( /*...*/ ) { $args = func_get_args(); array_unshift( $args, 'filerepo', $this->getName() ); + return call_user_func_array( 'wfMemcKey', $args ); } @@ -1680,13 +1803,13 @@ class FileRepo { ), 'thumb' => array( 'container' => $this->zones['thumb']['container'], - 'directory' => ( $this->zones['thumb']['directory'] == '' ) + 'directory' => $this->zones['thumb']['directory'] == '' ? 'temp' : $this->zones['thumb']['directory'] . '/temp' ), 'transcoded' => array( 'container' => $this->zones['transcoded']['container'], - 'directory' => ( $this->zones['transcoded']['directory'] == '' ) + 'directory' => $this->zones['transcoded']['directory'] == '' ? 'temp' : $this->zones['transcoded']['directory'] . '/temp' ) @@ -1701,7 +1824,7 @@ class FileRepo { /** * Get an UploadStash associated with this repo. * - * @param $user User + * @param User $user * @return UploadStash */ public function getUploadStash( User $user = null ) { @@ -1715,8 +1838,8 @@ class FileRepo { * @return void * @throws MWException */ - protected function assertWritableRepo() {} - + protected function assertWritableRepo() { + } /** * Return information about the repository. @@ -1725,12 +1848,24 @@ class FileRepo { * @since 1.22 */ public function getInfo() { - return array( + $ret = array( 'name' => $this->getName(), 'displayname' => $this->getDisplayName(), - 'rootUrl' => $this->getRootUrl(), + 'rootUrl' => $this->getZoneUrl( 'public' ), 'local' => $this->isLocal(), ); + + $optionalSettings = array( + 'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl', + 'fetchDescription', 'descriptionCacheExpiry', 'scriptExtension', 'favicon' + ); + foreach ( $optionalSettings as $k ) { + if ( isset( $this->$k ) ) { + $ret[$k] = $this->$k; + } + } + + return $ret; } } diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 6f28b104..56848df2 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -29,8 +29,7 @@ class FileRepoStatus extends Status { /** * Factory function for fatal errors * - * @param $repo FileRepo - * + * @param FileRepo $repo * @return FileRepoStatus */ static function newFatal( $repo /*, parameters...*/ ) { @@ -38,22 +37,24 @@ class FileRepoStatus extends Status { $result = new self( $repo ); call_user_func_array( array( &$result, 'error' ), $params ); $result->ok = false; + return $result; } /** - * @param $repo FileRepo - * @param $value + * @param FileRepo|bool $repo Default: false + * @param mixed $value * @return FileRepoStatus */ static function newGood( $repo = false, $value = null ) { $result = new self( $repo ); $result->value = $value; + return $result; } /** - * @param $repo FileRepo + * @param bool|FileRepo $repo */ function __construct( $repo = false ) { if ( $repo ) { diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 5eec9a50..6924f0a6 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -42,17 +42,31 @@ class ForeignAPIRepo extends FileRepo { * Update the version every time you make breaking or significant changes. */ const VERSION = "2.1"; - var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); - /* Check back with Commons after a day */ - var $apiThumbCacheExpiry = 86400; /* 24*60*60 */ - /* Redownload thumbnail files after a month */ - var $fileCacheExpiry = 2592000; /* 86400*30 */ + /** + * List of iiprop values for the thumbnail fetch queries. + * @since 1.23 + */ + protected static $imageInfoProps = array( + 'url', + 'thumbnail', + 'timestamp', + ); + + protected $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); + /** @var int Check back with Commons after a day (24*60*60) */ + protected $apiThumbCacheExpiry = 86400; + + /** @var int Redownload thumbnail files after a month (86400*30) */ + protected $fileCacheExpiry = 2592000; - protected $mQueryCache = array(); + /** @var array */ protected $mFileExists = array(); + /** @var array */ + private $mQueryCache = array(); + /** - * @param $info array|null + * @param array|null $info */ function __construct( $info ) { global $wgLocalFileRepo; @@ -92,19 +106,20 @@ class ForeignAPIRepo extends FileRepo { * Per docs in FileRepo, this needs to return false if we don't support versioned * files. Well, we don't. * - * @param $title Title - * @param $time string|bool + * @param Title $title + * @param string|bool $time * @return File */ function newFile( $title, $time = false ) { if ( $time ) { return false; } + return parent::newFile( $title, $time ); } /** - * @param $files array + * @param array $files * @return array */ function fileExistsBatch( array $files ) { @@ -126,8 +141,11 @@ class ForeignAPIRepo extends FileRepo { } } - $data = $this->fetchImageQuery( array( 'titles' => implode( $files, '|' ), - 'prop' => 'imageinfo' ) ); + $data = $this->fetchImageQuery( array( + 'titles' => implode( $files, '|' ), + 'prop' => 'imageinfo' ) + ); + if ( isset( $data['query']['pages'] ) ) { # First, get results from the query. Note we only care whether the image exists, # not whether it has a description page. @@ -151,11 +169,12 @@ class ForeignAPIRepo extends FileRepo { $results[$key] = $this->mFileExists[$file]; } } + return $results; } /** - * @param $virtualUrl string + * @param string $virtualUrl * @return bool */ function getFileProps( $virtualUrl ) { @@ -163,11 +182,11 @@ class ForeignAPIRepo extends FileRepo { } /** - * @param $query array + * @param array $query * @return string */ function fetchImageQuery( $query ) { - global $wgMemc, $wgLanguageCode; + global $wgLanguageCode; $query = array_merge( $query, array( @@ -190,7 +209,7 @@ class ForeignAPIRepo extends FileRepo { } /** - * @param $data array + * @param array $data * @return bool|array */ function getImageInfo( $data ) { @@ -201,11 +220,12 @@ class ForeignAPIRepo extends FileRepo { } } } + return false; } /** - * @param $hash string + * @param string $hash * @return array */ function findBySha1( $hash ) { @@ -224,21 +244,23 @@ class ForeignAPIRepo extends FileRepo { $ret[] = new ForeignAPIFile( Title::makeTitle( NS_FILE, $img['name'] ), $this, $img ); } } + return $ret; } /** - * @param $name string - * @param $width int - * @param $height int - * @param $result null - * @param $otherParams string + * @param string $name + * @param int $width + * @param int $height + * @param array $result Out parameter that will be changed by the function. + * @param string $otherParams + * * @return bool */ function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) { $data = $this->fetchImageQuery( array( 'titles' => 'File:' . $name, - 'iiprop' => 'url|timestamp', + 'iiprop' => self::getIIProps(), 'iiurlwidth' => $width, 'iiurlheight' => $height, 'iiurlparam' => $otherParams, @@ -248,6 +270,7 @@ class ForeignAPIRepo extends FileRepo { if ( $data && $info && isset( $info['thumburl'] ) ) { wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" ); $result = $info; + return $info['thumburl']; } else { return false; @@ -255,17 +278,18 @@ class ForeignAPIRepo extends FileRepo { } /** - * @param $name string - * @param $width int - * @param $height int - * @param $otherParams string + * @param string $name + * @param int $width + * @param int $height + * @param string $otherParams + * @param string $lang Language code for language of error * @return bool|MediaTransformError * @since 1.22 */ function getThumbError( $name, $width = -1, $height = -1, $otherParams = '', $lang = null ) { $data = $this->fetchImageQuery( array( 'titles' => 'File:' . $name, - 'iiprop' => 'url|timestamp', + 'iiprop' => self::getIIProps(), 'iiurlwidth' => $width, 'iiurlheight' => $height, 'iiurlparam' => $otherParams, @@ -276,6 +300,7 @@ class ForeignAPIRepo extends FileRepo { if ( $data && $info && isset( $info['thumberror'] ) ) { wfDebug( __METHOD__ . " got remote thumb error " . $info['thumberror'] . "\n" ); + return new MediaTransformError( 'thumbnail_error_remote', $width, @@ -294,10 +319,11 @@ class ForeignAPIRepo extends FileRepo { * If the url has been requested today, get it from cache * Otherwise retrieve remote thumb url, check for local file. * - * @param string $name is a dbkey form of a title - * @param $width - * @param $height - * @param string $params Other rendering parameters (page number, etc) from handler's makeParamString. + * @param string $name Is a dbkey form of a title + * @param int $width + * @param int $height + * @param string $params Other rendering parameters (page number, etc) + * from handler's makeParamString. * @return bool|string */ function getThumbUrlFromCache( $name, $width, $height, $params = "" ) { @@ -322,6 +348,7 @@ class ForeignAPIRepo extends FileRepo { if ( isset( $knownThumbUrls[$sizekey] ) ) { wfDebug( __METHOD__ . ': Got thumburl from local cache: ' . "{$knownThumbUrls[$sizekey]} \n" ); + return $knownThumbUrls[$sizekey]; } /* This size is not yet known */ @@ -332,6 +359,7 @@ class ForeignAPIRepo extends FileRepo { if ( !$foreignUrl ) { wfDebug( __METHOD__ . " Could not find thumburl\n" ); + return false; } @@ -339,14 +367,17 @@ class ForeignAPIRepo extends FileRepo { $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) ); if ( !$this->validateFilename( $fileName ) ) { wfDebug( __METHOD__ . " The deduced filename $fileName is not safe\n" ); + return false; } $localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name; $localFilename = $localPath . "/" . $fileName; - $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName ); + $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . + rawurlencode( $name ) . "/" . rawurlencode( $fileName ); if ( $backend->fileExists( array( 'src' => $localFilename ) ) - && isset( $metadata['timestamp'] ) ) { + && isset( $metadata['timestamp'] ) + ) { wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" ); $modified = $backend->getFileTimestamp( array( 'src' => $localFilename ) ); $remoteModified = strtotime( $metadata['timestamp'] ); @@ -356,6 +387,7 @@ class ForeignAPIRepo extends FileRepo { /* Use our current and already downloaded thumbnail */ $knownThumbUrls[$sizekey] = $localUrl; $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); + return $localUrl; } /* There is a new Commons file, or existing thumbnail older than a month */ @@ -363,6 +395,7 @@ class ForeignAPIRepo extends FileRepo { $thumb = self::httpGet( $foreignUrl ); if ( !$thumb ) { wfDebug( __METHOD__ . " Could not download thumb\n" ); + return false; } @@ -371,19 +404,21 @@ class ForeignAPIRepo extends FileRepo { $params = array( 'dst' => $localFilename, 'content' => $thumb ); if ( !$backend->quickCreate( $params )->isOK() ) { wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'\n" ); + return $foreignUrl; } $knownThumbUrls[$sizekey] = $localUrl; $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); + return $localUrl; } /** * @see FileRepo::getZoneUrl() - * @param $zone String + * @param string $zone * @param string|null $ext Optional file extension - * @return String + * @return string */ function getZoneUrl( $zone, $ext = null ) { switch ( $zone ) { @@ -398,7 +433,7 @@ class ForeignAPIRepo extends FileRepo { /** * Get the local directory corresponding to one of the basic zones - * @param $zone string + * @param string $zone * @return bool|null|string */ function getZonePath( $zone ) { @@ -406,6 +441,7 @@ class ForeignAPIRepo extends FileRepo { if ( in_array( $zone, $supported ) ) { return parent::getZonePath( $zone ); } + return false; } @@ -450,6 +486,10 @@ class ForeignAPIRepo extends FileRepo { $info['articlepath'] = $general['articlepath']; $info['server'] = $general['server']; + + if ( isset( $general['favicon'] ) ) { + $info['favicon'] = $general['favicon']; + } } return $info; @@ -458,10 +498,10 @@ class ForeignAPIRepo extends FileRepo { /** * Like a Http:get request, but with custom User-Agent. * @see Http:get - * @param $url string - * @param $timeout string - * @param $options array - * @return bool|String + * @param string $url + * @param string $timeout + * @param array $options + * @return bool|string */ public static function httpGet( $url, $timeout = 'default', $options = array() ) { $options['timeout'] = $timeout; @@ -486,10 +526,19 @@ class ForeignAPIRepo extends FileRepo { } /** + * @return string + * @since 1.23 + */ + protected static function getIIProps() { + return join( '|', self::$imageInfoProps ); + } + + /** * HTTP GET request to a mediawiki API (with caching) - * @param $target string Used in cache key creation, mostly - * @param $query array The query parameters for the API request - * @param $cacheTTL int Time to live for the memcached caching + * @param string $target Used in cache key creation, mostly + * @param array $query The query parameters for the API request + * @param int $cacheTTL Time to live for the memcached caching + * @return null */ public function httpGetCached( $target, $query, $cacheTTL = 3600 ) { if ( $this->mApiBase ) { @@ -526,7 +575,7 @@ class ForeignAPIRepo extends FileRepo { } /** - * @param $callback Array|string + * @param callable $callback * @throws MWException */ function enumFiles( $callback ) { diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 37c65723..6e9e6add 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -27,17 +27,37 @@ * @ingroup FileRepo */ class ForeignDBRepo extends LocalRepo { - # Settings - var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags, - $tablePrefix, $hasSharedCache; + /** @var string */ + protected $dbType; + + /** @var string */ + protected $dbServer; + + /** @var string */ + protected $dbUser; + + /** @var string */ + protected $dbPassword; + + /** @var string */ + protected $dbName; + + /** @var string */ + protected $dbFlags; + + /** @var string */ + protected $tablePrefix; + + /** @var bool */ + protected $hasSharedCache; # Other stuff - var $dbConn; - var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); - var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); + protected $dbConn; + protected $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); + protected $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); /** - * @param $info array|null + * @param array|null $info */ function __construct( $info ) { parent::__construct( $info ); @@ -68,6 +88,7 @@ class ForeignDBRepo extends LocalRepo { ) ); } + return $this->dbConn; } @@ -95,6 +116,7 @@ class ForeignDBRepo extends LocalRepo { if ( $this->hasSharedCache() ) { $args = func_get_args(); array_unshift( $args, $this->dbName, $this->tablePrefix ); + return call_user_func_array( 'wfForeignMemcKey', $args ); } else { return false; @@ -104,4 +126,14 @@ class ForeignDBRepo extends LocalRepo { protected function assertWritableRepo() { throw new MWException( get_class( $this ) . ': write operations are not supported.' ); } + + /** + * Return information about the repository. + * + * @return array + * @since 1.22 + */ + function getInfo() { + return FileRepo::getInfo(); + } } diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index 7951fb13..8153ffb4 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -27,12 +27,23 @@ * @ingroup FileRepo */ class ForeignDBViaLBRepo extends LocalRepo { - var $wiki, $dbName, $tablePrefix; - var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); - var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); + /** @var string */ + protected $wiki; + + /** @var string */ + protected $dbName; + + /** @var string */ + protected $tablePrefix; + + /** @var array */ + protected $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); + + /** @var array */ + protected $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); /** - * @param $info array|null + * @param array|null $info */ function __construct( $info ) { parent::__construct( $info ); @@ -69,6 +80,7 @@ class ForeignDBViaLBRepo extends LocalRepo { if ( $this->hasSharedCache() ) { $args = func_get_args(); array_unshift( $args, $this->wiki ); + return implode( ':', $args ); } else { return false; @@ -78,4 +90,8 @@ class ForeignDBViaLBRepo extends LocalRepo { protected function assertWritableRepo() { throw new MWException( get_class( $this ) . ': write operations are not supported.' ); } + + public function getInfo() { + return FileRepo::getInfo(); + } } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 9b62243b..926fd0b8 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -29,16 +29,27 @@ * @ingroup FileRepo */ class LocalRepo extends FileRepo { - var $fileFactory = array( 'LocalFile' , 'newFromTitle' ); - var $fileFactoryKey = array( 'LocalFile' , 'newFromKey' ); - var $fileFromRowFactory = array( 'LocalFile' , 'newFromRow' ); - var $oldFileFactory = array( 'OldLocalFile', 'newFromTitle' ); - var $oldFileFactoryKey = array( 'OldLocalFile', 'newFromKey' ); - var $oldFileFromRowFactory = array( 'OldLocalFile', 'newFromRow' ); + /** @var array */ + protected $fileFactory = array( 'LocalFile', 'newFromTitle' ); + + /** @var array */ + protected $fileFactoryKey = array( 'LocalFile', 'newFromKey' ); + + /** @var array */ + protected $fileFromRowFactory = array( 'LocalFile', 'newFromRow' ); + + /** @var array */ + protected $oldFileFromRowFactory = array( 'OldLocalFile', 'newFromRow' ); + + /** @var array */ + protected $oldFileFactory = array( 'OldLocalFile', 'newFromTitle' ); + + /** @var array */ + protected $oldFileFactoryKey = array( 'OldLocalFile', 'newFromKey' ); /** * @throws MWException - * @param $row + * @param stdClass $row * @return LocalFile */ function newFileFromRow( $row ) { @@ -52,8 +63,8 @@ class LocalRepo extends FileRepo { } /** - * @param $title - * @param $archiveName + * @param Title $title + * @param string $archiveName * @return OldLocalFile */ function newFromArchiveName( $title, $archiveName ) { @@ -66,7 +77,7 @@ class LocalRepo extends FileRepo { * interleave database locks with file operations, which is potentially a * remote operation. * - * @param $storageKeys array + * @param array $storageKeys * * @return FileRepoStatus */ @@ -80,7 +91,7 @@ class LocalRepo extends FileRepo { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; $dbw->begin( __METHOD__ ); - // Check for usage in deleted/hidden files and pre-emptively + // Check for usage in deleted/hidden files and preemptively // lock the key to avoid any future use until we are finished. $deleted = $this->deletedFileHasKey( $key, 'lock' ); $hidden = $this->hiddenFileHasKey( $key, 'lock' ); @@ -97,6 +108,7 @@ class LocalRepo extends FileRepo { } $dbw->commit( __METHOD__ ); } + return $status; } @@ -111,6 +123,7 @@ class LocalRepo extends FileRepo { $options = ( $lock === 'lock' ) ? array( 'FOR UPDATE' ) : array(); $dbw = $this->getMasterDB(); + return (bool)$dbw->selectField( 'filearchive', '1', array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), __METHOD__, $options @@ -131,6 +144,7 @@ class LocalRepo extends FileRepo { $ext = File::normalizeExtension( substr( $key, strcspn( $key, '.' ) + 1 ) ); $dbw = $this->getMasterDB(); + return (bool)$dbw->selectField( 'oldimage', '1', array( 'oi_sha1' => $sha1, 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ), @@ -152,8 +166,8 @@ class LocalRepo extends FileRepo { /** * Checks if there is a redirect named as $title * - * @param $title Title of file - * @return bool + * @param Title $title Title of file + * @return bool|Title */ function checkRedirect( Title $title ) { global $wgMemc; @@ -178,6 +192,7 @@ class LocalRepo extends FileRepo { $id = $this->getArticleID( $title ); if ( !$id ) { $wgMemc->add( $memcKey, " ", $expiry ); + return false; } $dbr = $this->getSlaveDB(); @@ -191,9 +206,11 @@ class LocalRepo extends FileRepo { if ( $row && $row->rd_namespace == NS_FILE ) { $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title ); $wgMemc->add( $memcKey, $targetTitle->getDBkey(), $expiry ); + return $targetTitle; } else { $wgMemc->add( $memcKey, '', $expiry ); + return false; } } @@ -202,7 +219,7 @@ class LocalRepo extends FileRepo { * Function link Title::getArticleID(). * We can't say Title object, what database it should use, so we duplicate that function here. * - * @param $title Title + * @param Title $title * @return bool|int|mixed */ protected function getArticleID( $title ) { @@ -219,15 +236,141 @@ class LocalRepo extends FileRepo { ), __METHOD__ //Function name ); + return $id; } + public function findFiles( array $items, $flags = 0 ) { + $finalFiles = array(); // map of (DB key => corresponding File) for matches + + $searchSet = array(); // map of (normalized DB key => search params) + foreach ( $items as $item ) { + if ( is_array( $item ) ) { + $title = File::normalizeTitle( $item['title'] ); + if ( $title ) { + $searchSet[$title->getDBkey()] = $item; + } + } else { + $title = File::normalizeTitle( $item ); + if ( $title ) { + $searchSet[$title->getDBkey()] = array(); + } + } + } + + $fileMatchesSearch = function ( File $file, array $search ) { + // Note: file name comparison done elsewhere (to handle redirects) + $user = ( !empty( $search['private'] ) && $search['private'] instanceof User ) + ? $search['private'] + : null; + + return ( + $file->exists() && + ( + ( empty( $search['time'] ) && !$file->isOld() ) || + ( !empty( $search['time'] ) && $search['time'] === $file->getTimestamp() ) + ) && + ( !empty( $search['private'] ) || !$file->isDeleted( File::DELETED_FILE ) ) && + $file->userCan( File::DELETED_FILE, $user ) + ); + }; + + $repo = $this; + $applyMatchingFiles = function ( ResultWrapper $res, &$searchSet, &$finalFiles ) + use ( $repo, $fileMatchesSearch, $flags ) + { + global $wgContLang; + $info = $repo->getInfo(); + foreach ( $res as $row ) { + $file = $repo->newFileFromRow( $row ); + // There must have been a search for this DB key, but this has to handle the + // cases were title capitalization is different on the client and repo wikis. + $dbKeysLook = array( str_replace( ' ', '_', $file->getName() ) ); + if ( !empty( $info['initialCapital'] ) ) { + // Search keys for "hi.png" and "Hi.png" should use the "Hi.png file" + $dbKeysLook[] = $wgContLang->lcfirst( $file->getName() ); + } + foreach ( $dbKeysLook as $dbKey ) { + if ( isset( $searchSet[$dbKey] ) + && $fileMatchesSearch( $file, $searchSet[$dbKey] ) + ) { + $finalFiles[$dbKey] = ( $flags & FileRepo::NAME_AND_TIME_ONLY ) + ? array( 'title' => $dbKey, 'timestamp' => $file->getTimestamp() ) + : $file; + unset( $searchSet[$dbKey] ); + } + } + } + }; + + $dbr = $this->getSlaveDB(); + + // Query image table + $imgNames = array(); + foreach ( array_keys( $searchSet ) as $dbKey ) { + $imgNames[] = $this->getNameFromTitle( File::normalizeTitle( $dbKey ) ); + } + + if ( count( $imgNames ) ) { + $res = $dbr->select( 'image', + LocalFile::selectFields(), array( 'img_name' => $imgNames ), __METHOD__ ); + $applyMatchingFiles( $res, $searchSet, $finalFiles ); + } + + // Query old image table + $oiConds = array(); // WHERE clause array for each file + foreach ( $searchSet as $dbKey => $search ) { + if ( isset( $search['time'] ) ) { + $oiConds[] = $dbr->makeList( + array( + 'oi_name' => $this->getNameFromTitle( File::normalizeTitle( $dbKey ) ), + 'oi_timestamp' => $dbr->timestamp( $search['time'] ) + ), + LIST_AND + ); + } + } + + if ( count( $oiConds ) ) { + $res = $dbr->select( 'oldimage', + OldLocalFile::selectFields(), $dbr->makeList( $oiConds, LIST_OR ), __METHOD__ ); + $applyMatchingFiles( $res, $searchSet, $finalFiles ); + } + + // Check for redirects... + foreach ( $searchSet as $dbKey => $search ) { + if ( !empty( $search['ignoreRedirect'] ) ) { + continue; + } + + $title = File::normalizeTitle( $dbKey ); + $redir = $this->checkRedirect( $title ); // hopefully hits memcached + + if ( $redir && $redir->getNamespace() == NS_FILE ) { + $file = $this->newFile( $redir ); + if ( $file && $fileMatchesSearch( $file, $search ) ) { + $file->redirectedFrom( $title->getDBkey() ); + if ( $flags & FileRepo::NAME_AND_TIME_ONLY ) { + $finalFiles[$dbKey] = array( + 'title' => $file->getTitle()->getDBkey(), + 'timestamp' => $file->getTimestamp() + ); + } else { + $finalFiles[$dbKey] = $file; + } + } + } + } + + return $finalFiles; + } + /** * Get an array or iterator of file objects for files that have a given * SHA-1 content hash. * - * @param string $hash a sha1 hash to look for - * @return Array + * @param string $hash A sha1 hash to look for + * @return File[] */ function findBySha1( $hash ) { $dbr = $this->getSlaveDB(); @@ -299,13 +442,14 @@ class LocalRepo extends FileRepo { 'img_name ' . $dbr->buildLike( $prefix, $dbr->anyString() ), __METHOD__, $selectOptions - ); + ); // Build file objects $files = array(); foreach ( $res as $row ) { $files[] = $this->newFileFromRow( $row ); } + return $files; } @@ -334,13 +478,14 @@ class LocalRepo extends FileRepo { */ function getSharedCacheKey( /*...*/ ) { $args = func_get_args(); + return call_user_func_array( 'wfMemcKey', $args ); } /** * Invalidates image redirect cache related to that image * - * @param $title Title of page + * @param Title $title Title of page * @return void */ function invalidateImageRedirect( Title $title ) { @@ -354,4 +499,18 @@ class LocalRepo extends FileRepo { $wgMemc->set( $memcKey, ' PURGED', 12 ); } } + + /** + * Return information about the repository. + * + * @return array + * @since 1.22 + */ + function getInfo() { + global $wgFavicon; + + return array_merge( parent::getInfo(), array( + 'favicon' => wfExpandUrl( $wgFavicon ), + ) ); + } } diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index dda51cea..f2b7395c 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -26,11 +26,11 @@ * @ingroup FileRepo */ class NullRepo extends FileRepo { - /** - * @param $info array|null + * @param array|null $info */ - function __construct( $info ) {} + function __construct( $info ) { + } protected function assertWritableRepo() { throw new MWException( get_class( $this ) . ': write operations are not supported.' ); diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index b2b9477a..fab42162 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -27,19 +27,28 @@ * @ingroup FileRepo */ class RepoGroup { - /** - * @var LocalRepo - */ - var $localRepo; + /** @var LocalRepo */ + protected $localRepo; - var $foreignRepos, $reposInitialised = false; - var $localInfo, $foreignInfo; - var $cache; + /** @var FileRepo[] */ + protected $foreignRepos; - /** - * @var RepoGroup - */ + /** @var bool */ + protected $reposInitialised = false; + + /** @var array */ + protected $localInfo; + + /** @var array */ + protected $foreignInfo; + + /** @var ProcessCacheLRU */ + protected $cache; + + /** @var RepoGroup */ protected static $instance; + + /** Maximum number of cache items */ const MAX_CACHE_SIZE = 500; /** @@ -53,6 +62,7 @@ class RepoGroup { } global $wgLocalFileRepo, $wgForeignFileRepos; self::$instance = new RepoGroup( $wgLocalFileRepo, $wgForeignFileRepos ); + return self::$instance; } @@ -70,7 +80,7 @@ class RepoGroup { * It's not enough to just create a superclass ... you have * to get people to call into it even though all they know is RepoGroup::singleton() * - * @param $instance RepoGroup + * @param RepoGroup $instance */ static function setSingleton( $instance ) { self::$instance = $instance; @@ -80,35 +90,32 @@ class RepoGroup { * Construct a group of file repositories. * * @param array $localInfo Associative array for local repo's info - * @param array $foreignInfo of repository info arrays. - * Each info array is an associative array with the 'class' member - * giving the class name. The entire array is passed to the repository - * constructor as the first parameter. + * @param array $foreignInfo Array of repository info arrays. + * Each info array is an associative array with the 'class' member + * giving the class name. The entire array is passed to the repository + * constructor as the first parameter. */ function __construct( $localInfo, $foreignInfo ) { $this->localInfo = $localInfo; $this->foreignInfo = $foreignInfo; - $this->cache = array(); + $this->cache = new ProcessCacheLRU( self::MAX_CACHE_SIZE ); } /** * Search repositories for an image. * You can also use wfFindFile() to do this. * - * @param $title Title|string Title object or string + * @param Title|string $title Title object or string * @param array $options Associative array of options: - * time: requested time for an archived image, or false for the - * current version. An image object will be returned which was - * created at the specified time. - * - * ignoreRedirect: If true, do not follow file redirects - * - * private: If true, return restricted (deleted) files if the current - * user is allowed to view them. Otherwise, such files will not - * be found. - * - * bypassCache: If true, do not use the process-local cache of File objects - * @return File object or false if it is not found + * time: requested time for an archived image, or false for the + * current version. An image object will be returned which was + * created at the specified time. + * ignoreRedirect: If true, do not follow file redirects + * private: If true, return restricted (deleted) files if the current + * user is allowed to view them. Otherwise, such files will not + * be found. + * bypassCache: If true, do not use the process-local cache of File objects + * @return File|bool False if title is not found */ function findFile( $title, $options = array() ) { if ( !is_array( $options ) ) { @@ -126,16 +133,12 @@ class RepoGroup { # Check the cache if ( empty( $options['ignoreRedirect'] ) && empty( $options['private'] ) - && empty( $options['bypassCache'] ) ) - { + && empty( $options['bypassCache'] ) + ) { $time = isset( $options['time'] ) ? $options['time'] : ''; $dbkey = $title->getDBkey(); - if ( isset( $this->cache[$dbkey][$time] ) ) { - wfDebug( __METHOD__ . ": got File:$dbkey from process cache\n" ); - # Move it to the end of the list so that we can delete the LRU entry later - $this->pingCache( $dbkey ); - # Return the entry - return $this->cache[$dbkey][$time]; + if ( $this->cache->has( $dbkey, $time, 60 ) ) { + return $this->cache->get( $dbkey, $time ); } $useCache = true; } else { @@ -158,18 +161,30 @@ class RepoGroup { $image = $image ? $image : false; // type sanity # Cache file existence or non-existence if ( $useCache && ( !$image || $image->isCacheable() ) ) { - $this->trimCache(); - $this->cache[$dbkey][$time] = $image; + $this->cache->set( $dbkey, $time, $image ); } return $image; } /** - * @param $inputItems array - * @return array + * Search repositories for many files at once. + * + * @param array $inputItems An array of titles, or an array of findFile() options with + * the "title" option giving the title. Example: + * + * $findItem = array( 'title' => $title, 'private' => true ); + * $findBatch = array( $findItem ); + * $repo->findFiles( $findBatch ); + * + * No title should appear in $items twice, as the result use titles as keys + * @param int $flags Supports: + * - FileRepo::NAME_AND_TIME_ONLY : return a (search title => (title,timestamp)) map. + * The search title uses the input titles; the other is the final post-redirect title. + * All titles are returned as string DB keys and the inner array is associative. + * @return array Map of (file name => File objects) for matches */ - function findFiles( $inputItems ) { + function findFiles( array $inputItems, $flags = 0 ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } @@ -185,7 +200,7 @@ class RepoGroup { } } - $images = $this->localRepo->findFiles( $items ); + $images = $this->localRepo->findFiles( $items, $flags ); foreach ( $this->foreignRepos as $repo ) { // Remove found files from $items @@ -193,15 +208,16 @@ class RepoGroup { unset( $items[$name] ); } - $images = array_merge( $images, $repo->findFiles( $items ) ); + $images = array_merge( $images, $repo->findFiles( $items, $flags ) ); } + return $images; } /** * Interface for FileRepo::checkRedirect() - * @param $title Title - * @return bool + * @param Title $title + * @return bool|Title */ function checkRedirect( Title $title ) { if ( !$this->reposInitialised ) { @@ -212,12 +228,14 @@ class RepoGroup { if ( $redir ) { return $redir; } + foreach ( $this->foreignRepos as $repo ) { $redir = $repo->checkRedirect( $title ); if ( $redir ) { return $redir; } } + return false; } @@ -225,9 +243,9 @@ class RepoGroup { * Find an instance of the file with this key, created at the specified time * Returns false if the file does not exist. * - * @param string $hash base 36 SHA-1 hash + * @param string $hash Base 36 SHA-1 hash * @param array $options Option array, same as findFile() - * @return File object or false if it is not found + * @return File|bool File object or false if it is not found */ function findFileFromKey( $hash, $options = array() ) { if ( !$this->reposInitialised ) { @@ -243,14 +261,15 @@ class RepoGroup { } } } + return $file; } /** * Find all instances of files with this key * - * @param string $hash base 36 SHA-1 hash - * @return Array of File objects + * @param string $hash Base 36 SHA-1 hash + * @return File[] */ function findBySha1( $hash ) { if ( !$this->reposInitialised ) { @@ -262,14 +281,15 @@ class RepoGroup { $result = array_merge( $result, $repo->findBySha1( $hash ) ); } usort( $result, 'File::compare' ); + return $result; } /** * Find all instances of files with this keys * - * @param array $hashes base 36 SHA-1 hashes - * @return Array of array of File objects + * @param array $hashes Base 36 SHA-1 hashes + * @return array Array of array of File objects */ function findBySha1s( array $hashes ) { if ( !$this->reposInitialised ) { @@ -284,12 +304,13 @@ class RepoGroup { foreach ( $result as $hash => $files ) { usort( $result[$hash], 'File::compare' ); } + return $result; } /** * Get the repo instance with a given key. - * @param $index string|int + * @param string|int $index * @return bool|LocalRepo */ function getRepo( $index ) { @@ -307,7 +328,7 @@ class RepoGroup { /** * Get the repo instance by its name - * @param $name string + * @param string $name * @return bool */ function getRepoByName( $name ) { @@ -319,6 +340,7 @@ class RepoGroup { return $repo; } } + return false; } @@ -336,25 +358,32 @@ class RepoGroup { * Call a function for each foreign repo, with the repo object as the * first parameter. * - * @param $callback Callback: the function to call - * @param array $params optional additional parameters to pass to the function + * @param callable $callback The function to call + * @param array $params Optional additional parameters to pass to the function * @return bool */ function forEachForeignRepo( $callback, $params = array() ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } foreach ( $this->foreignRepos as $repo ) { $args = array_merge( array( $repo ), $params ); if ( call_user_func_array( $callback, $args ) ) { return true; } } + return false; } /** * Does the installation have any foreign repos set up? - * @return Boolean + * @return bool */ function hasForeignRepos() { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } return (bool)$this->foreignRepos; } @@ -376,17 +405,20 @@ class RepoGroup { /** * Create a repo class based on an info structure + * @param array $info + * @return FileRepo */ protected function newRepo( $info ) { $class = $info['class']; + return new $class( $info ); } /** * Split a virtual URL into repo, zone and rel parts - * @param $url string + * @param string $url * @throws MWException - * @return array containing repo, zone and rel + * @return array Containing repo, zone and rel */ function splitVirtualUrl( $url ) { if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { @@ -397,11 +429,12 @@ class RepoGroup { if ( count( $bits ) != 3 ) { throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" ); } + return $bits; } /** - * @param $fileName string + * @param string $fileName * @return array */ function getFileProps( $fileName ) { @@ -411,6 +444,7 @@ class RepoGroup { $repoName = 'local'; } $repo = $this->getRepo( $repoName ); + return $repo->getFileProps( $fileName ); } else { return FSFile::getPropsFromPath( $fileName ); @@ -418,40 +452,14 @@ class RepoGroup { } /** - * Move a cache entry to the top (such as when accessed) - */ - protected function pingCache( $key ) { - if ( isset( $this->cache[$key] ) ) { - $tmp = $this->cache[$key]; - unset( $this->cache[$key] ); - $this->cache[$key] = $tmp; - } - } - - /** - * Limit cache memory - */ - protected function trimCache() { - while ( count( $this->cache ) >= self::MAX_CACHE_SIZE ) { - reset( $this->cache ); - $key = key( $this->cache ); - wfDebug( __METHOD__ . ": evicting $key\n" ); - unset( $this->cache[$key] ); - } - } - - /** * Clear RepoGroup process cache used for finding a file - * @param $title Title|null Title of the file or null to clear all files + * @param Title|null $title Title of the file or null to clear all files */ public function clearCache( Title $title = null ) { if ( $title == null ) { - $this->cache = array(); + $this->cache->clear(); } else { - $dbKey = $title->getDBkey(); - if ( isset( $this->cache[$dbKey] ) ) { - unset( $this->cache[$dbKey] ); - } + $this->cache->clear( $title->getDBkey() ); } } } 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 ) { |