From ca32f08966f1b51fcb19460f0996bb0c4048e6fe Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 3 Dec 2011 13:29:22 +0100 Subject: Update to MediaWiki 1.18.0 * also update ArchLinux skin to chagnes in MonoBook * Use only css to hide our menu bar when printing --- includes/filerepo/ArchivedFile.php | 69 +++- includes/filerepo/FSRepo.php | 122 ++++++- includes/filerepo/File.php | 541 ++++++++++++++++++++-------- includes/filerepo/FileRepo.php | 122 +++---- includes/filerepo/FileRepoStatus.php | 12 + includes/filerepo/ForeignAPIFile.php | 29 +- includes/filerepo/ForeignAPIRepo.php | 51 ++- includes/filerepo/ForeignDBFile.php | 23 +- includes/filerepo/ForeignDBRepo.php | 4 +- includes/filerepo/Image.php | 80 ---- includes/filerepo/LocalFile.php | 273 +++++++++++--- includes/filerepo/LocalRepo.php | 37 +- includes/filerepo/NullRepo.php | 3 + includes/filerepo/OldLocalFile.php | 100 ++++- includes/filerepo/README | 18 + includes/filerepo/RepoGroup.php | 56 ++- includes/filerepo/UnregisteredLocalFile.php | 24 +- 17 files changed, 1144 insertions(+), 420 deletions(-) delete mode 100644 includes/filerepo/Image.php (limited to 'includes/filerepo') diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index ecc09978..0d9e349b 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -16,7 +16,6 @@ class ArchivedFile { * @private */ var $id, # filearchive row ID - $title, # image title $name, # image name $group, # FileStore storage group $key, # FileStore sha1 key @@ -32,10 +31,27 @@ class ArchivedFile { $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 + $deleted, # Bitfield akin to rev_deleted + $pageCount, + $archive_name; + + /** + * @var MediaHandler + */ + var $handler; + /** + * @var Title + */ + var $title; # image title /**#@-*/ + /** + * @throws MWException + * @param Title $title + * @param int $id + * @param string $key + */ function __construct( $title, $id=0, $key='' ) { $this->id = -1; $this->title = false; @@ -57,19 +73,22 @@ class ArchivedFile { $this->dataLoaded = false; $this->exists = false; - if( is_object($title) ) { + if( is_object( $title ) ) { $this->title = $title; $this->name = $title->getDBkey(); } - if ($id) + if ($id) { $this->id = $id; + } - if ($key) + if ($key) { $this->key = $key; + } - if (!$id && !$key && !is_object($title)) + if ( !$id && !$key && !is_object( $title ) ) { throw new MWException( "No specifications provided to ArchivedFile constructor." ); + } } /** @@ -82,17 +101,20 @@ class ArchivedFile { } $conds = array(); - if( $this->id > 0 ) + if( $this->id > 0 ) { $conds['fa_id'] = $this->id; + } if( $this->key ) { $conds['fa_storage_group'] = $this->group; $conds['fa_storage_key'] = $this->key; } - if( $this->title ) + if( $this->title ) { $conds['fa_name'] = $this->title->getDBkey(); + } - if( !count($conds)) + if( !count($conds)) { throw new MWException( "No specific information for retrieving archived file" ); + } if( !$this->title || $this->title->getNamespace() == NS_FILE ) { $dbr = wfGetDB( DB_SLAVE ); @@ -119,8 +141,7 @@ class ArchivedFile { $conds, __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - - if ( $dbr->numRows( $res ) == 0 ) { + if ( $res == false || $dbr->numRows( $res ) == 0 ) { // this revision does not exist? return; } @@ -276,6 +297,32 @@ class ArchivedFile { return $this->mime; } + /** + * Get a MediaHandler instance for this file + * @return MediaHandler + */ + function getHandler() { + 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 + */ + function pageCount() { + if ( !isset( $this->pageCount ) ) { + if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) { + $this->pageCount = $this->handler->pageCount( $this ); + } else { + $this->pageCount = false; + } + } + return $this->pageCount; + } + /** * Return the type of the media in the file. * Use the value returned by this function with the MEDIATYPE_xxx constants. diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index e2251b2b..2610ac6e 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -65,6 +65,10 @@ class FSRepo extends FileRepo { /** * Get the local directory corresponding to one of the three basic zones + * + * @param $zone string + * + * @return string */ function getZonePath( $zone ) { switch ( $zone ) { @@ -83,6 +87,10 @@ class FSRepo extends FileRepo { /** * @see FileRepo::getZoneUrl() + * + * @param $zone string + * + * @return url */ function getZoneUrl( $zone ) { switch ( $zone ) { @@ -103,6 +111,10 @@ class FSRepo extends FileRepo { * Get a URL referring to this repository, with the private mwrepo protocol. * The suffix, if supplied, is considered to be unencoded, and will be * URL-encoded before being returned. + * + * @param $suffix string + * + * @return string */ function getVirtualUrl( $suffix = false ) { $path = 'mwrepo://' . $this->name; @@ -114,10 +126,14 @@ class FSRepo extends FileRepo { /** * Get the local path corresponding to a virtual URL + * + * @param $url string + * + * @return string */ function resolveVirtualUrl( $url ) { if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { - throw new MWException( __METHOD__.': unknown protoocl' ); + throw new MWException( __METHOD__.': unknown protocol' ); } $bits = explode( '/', substr( $url, 9 ), 3 ); @@ -146,16 +162,23 @@ class FSRepo extends FileRepo { * same contents as the source */ function storeBatch( $triplets, $flags = 0 ) { + wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) . + " triplets; flags: {$flags}\n" ); + + // Try creating directories if ( !wfMkdirParents( $this->directory ) ) { return $this->newFatal( 'upload_directory_missing', $this->directory ); } if ( !is_writable( $this->directory ) ) { return $this->newFatal( 'upload_directory_read_only', $this->directory ); } + + // Validate each triplet $status = $this->newGood(); foreach ( $triplets as $i => $triplet ) { list( $srcPath, $dstZone, $dstRel ) = $triplet; + // Resolve destination path $root = $this->getZonePath( $dstZone ); if ( !$root ) { throw new MWException( "Invalid zone: $dstZone" ); @@ -166,6 +189,7 @@ class FSRepo extends FileRepo { $dstPath = "$root/$dstRel"; $dstDir = dirname( $dstPath ); + // Create destination directories for this triplet if ( !is_dir( $dstDir ) ) { if ( !wfMkdirParents( $dstDir ) ) { return $this->newFatal( 'directorycreateerror', $dstDir ); @@ -175,6 +199,7 @@ class FSRepo extends FileRepo { } } + // Resolve source if ( self::isVirtualUrl( $srcPath ) ) { $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath ); } @@ -183,6 +208,8 @@ class FSRepo extends FileRepo { $status->fatal( 'filenotfound', $srcPath ); continue; } + + // Check overwriting if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) { if ( $flags & self::OVERWRITE_SAME ) { $hashSource = sha1_file( $srcPath ); @@ -196,6 +223,7 @@ class FSRepo extends FileRepo { } } + // Windows does not support moving over existing files, so explicitly delete them $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE ); // Abort now on failure @@ -203,7 +231,8 @@ class FSRepo extends FileRepo { return $status; } - foreach ( $triplets as $triplet ) { + // Execute the store operation for each triplet + foreach ( $triplets as $i => $triplet ) { list( $srcPath, $dstZone, $dstRel ) = $triplet; $root = $this->getZonePath( $dstZone ); $dstPath = "$root/$dstRel"; @@ -222,6 +251,20 @@ class FSRepo extends FileRepo { $status->error( 'filecopyerror', $srcPath, $dstPath ); $good = false; } + if ( !( $flags & self::SKIP_VALIDATION ) ) { + wfSuppressWarnings(); + $hashSource = sha1_file( $srcPath ); + $hashDest = sha1_file( $dstPath ); + wfRestoreWarnings(); + + if ( $hashDest === false || $hashSource !== $hashDest ) { + wfDebug( __METHOD__ . ': File copy validation failed: ' . + "$srcPath ($hashSource) to $dstPath ($hashDest)\n" ); + + $status->error( 'filecopyerror', $srcPath, $dstPath ); + $good = false; + } + } } if ( $good ) { $this->chmod( $dstPath ); @@ -229,47 +272,81 @@ class FSRepo extends FileRepo { } else { $status->failCount++; } + $status->success[$i] = $good; } return $status; } + + /** + * Deletes a batch of files. Each file can be a (zone, rel) pairs, a + * virtual url or a real path. It will try to delete each file, but + * ignores any errors that may occur + * + * @param $pairs array List of files to delete + */ + function cleanupBatch( $files ) { + foreach ( $files as $file ) { + if ( is_array( $file ) ) { + // This is a pair, extract it + list( $zone, $rel ) = $file; + $root = $this->getZonePath( $zone ); + $path = "$root/$rel"; + } else { + if ( self::isVirtualUrl( $file ) ) { + // This is a virtual url, resolve it + $path = $this->resolveVirtualUrl( $file ); + } else { + // This is a full file name + $path = $file; + } + } + + wfSuppressWarnings(); + unlink( $path ); + wfRestoreWarnings(); + } + } function append( $srcPath, $toAppendPath, $flags = 0 ) { $status = $this->newGood(); // Resolve the virtual URL - if ( self::isVirtualUrl( $srcPath ) ) { - $srcPath = $this->resolveVirtualUrl( $srcPath ); + if ( self::isVirtualUrl( $toAppendPath ) ) { + $toAppendPath = $this->resolveVirtualUrl( $toAppendPath ); } // Make sure the files are there - if ( !is_file( $srcPath ) ) - $status->fatal( 'filenotfound', $srcPath ); - if ( !is_file( $toAppendPath ) ) $status->fatal( 'filenotfound', $toAppendPath ); + if ( !is_file( $srcPath ) ) + $status->fatal( 'filenotfound', $srcPath ); + if ( !$status->isOk() ) return $status; // Do the append - $chunk = file_get_contents( $toAppendPath ); + $chunk = file_get_contents( $srcPath ); if( $chunk === false ) { - $status->fatal( 'fileappenderrorread', $toAppendPath ); + $status->fatal( 'fileappenderrorread', $srcPath ); } if( $status->isOk() ) { - if ( file_put_contents( $srcPath, $chunk, FILE_APPEND ) ) { - $status->value = $srcPath; + if ( file_put_contents( $toAppendPath, $chunk, FILE_APPEND ) ) { + $status->value = $toAppendPath; } else { - $status->fatal( 'fileappenderror', $toAppendPath, $srcPath); + $status->fatal( 'fileappenderror', $srcPath, $toAppendPath); } } if ( $flags & self::DELETE_SOURCE ) { - unlink( $toAppendPath ); + unlink( $srcPath ); } return $status; } + /* We can actually append to the files, so no-op needed here. */ + function appendFinish( $toAppendPath ) {} + /** * Checks existence of specified array of files. * @@ -515,14 +592,18 @@ class FSRepo extends FileRepo { $good = true; if ( file_exists( $archivePath ) ) { # A file with this content hash is already archived - if ( !@unlink( $srcPath ) ) { + wfSuppressWarnings(); + $good = unlink( $srcPath ); + wfRestoreWarnings(); + if ( !$good ) { $status->error( 'filedeleteerror', $srcPath ); - $good = false; } } else{ - if ( !@rename( $srcPath, $archivePath ) ) { + wfSuppressWarnings(); + $good = rename( $srcPath, $archivePath ); + wfRestoreWarnings(); + if ( !$good ) { $status->error( 'filerenameerror', $srcPath, $archivePath ); - $good = false; } else { $this->chmod( $archivePath ); } @@ -564,8 +645,11 @@ class FSRepo extends FileRepo { continue; } $dir = opendir( $path ); - while ( false !== ( $name = readdir( $dir ) ) ) { - call_user_func( $callback, $path . '/' . $name ); + if ($dir) { + while ( false !== ( $name = readdir( $dir ) ) ) { + call_user_func( $callback, $path . '/' . $name ); + } + closedir( $dir ); } } } diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 192e8c8a..6b35102b 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -52,7 +52,23 @@ abstract class File { /** * The following member variables are not lazy-initialised */ - var $repo, $title, $lastError, $redirected, $redirectedTitle; + + /** + * @var LocalRepo + */ + var $repo; + + /** + * @var Title + */ + var $title; + + var $lastError, $redirected, $redirectedTitle; + + /** + * @var MediaHandler + */ + protected $handler; /** * Call this constructor from child classes @@ -101,6 +117,8 @@ abstract class File { * * @param $old File Old file * @param $new string New name + * + * @return bool|null */ static function checkExtensionCompatibility( File $old, $new ) { $oldMime = $old->getMimeType(); @@ -122,10 +140,10 @@ abstract class File { * Split an internet media type into its two components; if not * a two-part name, set the minor type to 'unknown'. * - * @param $mime "text/html" etc + * @param string $mime "text/html" etc * @return array ("text", "html") etc */ - static function splitMime( $mime ) { + public static function splitMime( $mime ) { if( strpos( $mime, '/' ) !== false ) { return explode( '/', $mime, 2 ); } else { @@ -135,6 +153,8 @@ abstract class File { /** * Return the name of this file + * + * @return string */ public function getName() { if ( !isset( $this->name ) ) { @@ -145,6 +165,8 @@ abstract class File { /** * Get the file extension, e.g. "svg" + * + * @return string */ function getExtension() { if ( !isset( $this->extension ) ) { @@ -157,20 +179,26 @@ abstract class File { /** * Return the associated title object + * @return Title */ public function getTitle() { return $this->title; } - + /** * Return the title used to find this file + * + * @return Title */ public function getOriginalTitle() { - if ( $this->redirected ) + if ( $this->redirected ) { return $this->getRedirectedTitle(); + } return $this->title; } /** * Return the URL of the file + * + * @return string */ public function getUrl() { if ( !isset( $this->url ) ) { @@ -187,15 +215,21 @@ abstract class File { * @return String */ public function getFullUrl() { - return wfExpandUrl( $this->getUrl() ); + return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE ); + } + + public function getCanonicalUrl() { + return wfExpandUrl( $this->getUrl(), PROTO_CANONICAL ); } + /** + * @return string + */ function getViewURL() { if( $this->mustRender()) { if( $this->canRender() ) { return $this->createThumb( $this->getWidth() ); - } - else { + } else { wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n"); return $this->getURL(); #hm... return NULL? } @@ -212,7 +246,10 @@ abstract class File { * i.e. whether the files are all found in the same directory, * or in hashed paths like /images/3/3c. * - * May return false if the file is not locally accessible. + * Most callers don't check the return value, but ForeignAPIFile::getPath + * returns false. + * + * @return string|false */ public function getPath() { if ( !isset( $this->path ) ) { @@ -222,9 +259,14 @@ abstract class File { } /** - * Alias for getPath() - */ + * Alias for getPath() + * + * @deprecated since 1.18 Use getPath(). + * + * @return string + */ public function getFullPath() { + wfDeprecated( __METHOD__ ); return $this->getPath(); } @@ -234,8 +276,14 @@ abstract class File { * * STUB * Overridden by LocalFile, UnregisteredLocalFile + * + * @param $page int + * + * @return number */ - public function getWidth( $page = 1 ) { return false; } + public function getWidth( $page = 1 ) { + return false; + } /** * Return the height of the image. Returns false if the height is unknown @@ -243,19 +291,29 @@ abstract class File { * * STUB * Overridden by LocalFile, UnregisteredLocalFile + * + * @return false|number */ - public function getHeight( $page = 1 ) { return false; } + public function getHeight( $page = 1 ) { + return false; + } /** * Returns ID or name of user who uploaded the file * STUB * * @param $type string 'text' or 'id' + * + * @return string|int */ - public function getUser( $type='text' ) { return null; } + public function getUser( $type = 'text' ) { + return null; + } /** * Get the duration of a media file in seconds + * + * @return number */ public function getLength() { $handler = $this->getHandler(); @@ -267,45 +325,76 @@ abstract class File { } /** - * Return true if the file is vectorized - */ - public function isVectorized() { - $handler = $this->getHandler(); - if ( $handler ) { - return $handler->isVectorized( $this ); - } else { - return false; - } - } - + * Return true if the file is vectorized + * + * @return bool + */ + public function isVectorized() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->isVectorized( $this ); + } else { + return false; + } + } /** * Get handler-specific metadata * Overridden by LocalFile, UnregisteredLocalFile * STUB */ - public function getMetadata() { return false; } + public function getMetadata() { + return false; + } + + /** + * 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) + */ + public function convertMetadataVersion($metadata, $version) { + $handler = $this->getHandler(); + if ( !is_array( $metadata ) ) { + //just to make the return type consistant + $metadata = unserialize( $metadata ); + } + if ( $handler ) { + return $handler->convertMetadataVersion( $metadata, $version ); + } else { + return $metadata; + } + } /** * Return the bit depth of the file * Overridden by LocalFile * STUB */ - public function getBitDepth() { return 0; } + public function getBitDepth() { + return 0; + } /** * Return the size of the image file, in bytes * Overridden by LocalFile, UnregisteredLocalFile * STUB */ - public function getSize() { return false; } + public function getSize() { + return false; + } /** * Returns the mime type of the file. * Overridden by LocalFile, UnregisteredLocalFile * STUB + * + * @return string */ - function getMimeType() { return 'unknown/unknown'; } + function getMimeType() { + return 'unknown/unknown'; + } /** * Return the type of the media in the file. @@ -324,6 +413,8 @@ abstract class File { * that can be converted to a format * supported by all browsers (namely GIF, PNG and JPEG), * or if it is an SVG image and SVG conversion is enabled. + * + * @return bool */ function canRender() { if ( !isset( $this->canRender ) ) { @@ -355,6 +446,8 @@ abstract class File { /** * Alias for canRender() + * + * @return bool */ function allowInlineDisplay() { return $this->canRender(); @@ -370,6 +463,8 @@ abstract class File { * * Note that this function will always return true if allowInlineDisplay() * or isTrustedFile() is true for this file. + * + * @return bool */ function isSafeFile() { if ( !isset( $this->isSafeFile ) ) { @@ -378,41 +473,64 @@ abstract class File { return $this->isSafeFile; } - /** Accessor for __get() */ + /** + * Accessor for __get() + * + * @return bool + */ protected function getIsSafeFile() { return $this->isSafeFile(); } - /** Uncached accessor */ + /** + * Uncached accessor + * + * @return bool + */ protected function _getIsSafeFile() { - if ($this->allowInlineDisplay()) return true; - if ($this->isTrustedFile()) return true; + if ( $this->allowInlineDisplay() ) { + return true; + } + if ($this->isTrustedFile()) { + return true; + } global $wgTrustedMediaFormats; - $type= $this->getMediaType(); - $mime= $this->getMimeType(); + $type = $this->getMediaType(); + $mime = $this->getMimeType(); #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n"); - if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted - if ( in_array( $type, $wgTrustedMediaFormats) ) return true; + if ( !$type || $type === MEDIATYPE_UNKNOWN ) { + return false; #unknown type, not trusted + } + if ( in_array( $type, $wgTrustedMediaFormats ) ) { + return true; + } - if ($mime==="unknown/unknown") return false; #unknown type, not trusted - if ( in_array( $mime, $wgTrustedMediaFormats) ) return true; + if ( $mime === "unknown/unknown" ) { + return false; #unknown type, not trusted + } + if ( in_array( $mime, $wgTrustedMediaFormats) ) { + return true; + } return false; } - /** Returns true if the file is flagged as trusted. Files flagged that way - * can be linked to directly, even if that is not allowed for this type of - * file normally. - * - * This is a dummy function right now and always returns false. It could be - * implemented to extract a flag from the database. The trusted flag could be - * set on upload, if the user has sufficient privileges, to bypass script- - * and html-filters. It may even be coupled with cryptographics signatures - * or such. - */ + /** + * Returns true if the file is flagged as trusted. Files flagged that way + * can be linked to directly, even if that is not allowed for this type of + * file normally. + * + * This is a dummy function right now and always returns false. It could be + * implemented to extract a flag from the database. The trusted flag could be + * set on upload, if the user has sufficient privileges, to bypass script- + * and html-filters. It may even be coupled with cryptographics signatures + * or such. + * + * @return bool + */ function isTrustedFile() { #this could be implemented to check a flag in the databas, #look for signatures, etc @@ -435,12 +553,14 @@ abstract class File { * It would be unsafe to include private images, making public thumbnails inadvertently * * @return boolean Whether file exists in the repository and is includable. - * @public */ - function isVisible() { + public function isVisible() { return $this->exists(); } + /** + * @return string + */ function getTransformScript() { if ( !isset( $this->transformScript ) ) { $this->transformScript = false; @@ -456,6 +576,10 @@ abstract class File { /** * Get a ThumbnailImage which is the same size as the source + * + * @param $handlerParams array + * + * @return string */ function getUnscaledThumb( $handlerParams = array() ) { $hp =& $handlerParams; @@ -473,14 +597,28 @@ abstract class File { * * @param $params Array: handler-specific parameters * @private -ish + * + * @return string */ function thumbName( $params ) { + return $this->generateThumbName( $this->getName(), $params ); + } + + /** + * Generate a thumbnail file name from a name and specified parameters + * + * @param string $name + * @param array $params Parameters which will be passed to MediaHandler::makeParamString + * + * @return string + */ + function generateThumbName( $name, $params ) { if ( !$this->getHandler() ) { return null; } $extension = $this->getExtension(); list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); - $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName(); + $thumbName = $this->handler->makeParamString( $params ) . '-' . $name; if ( $thumbExt != $extension ) { $thumbName .= ".$thumbExt"; } @@ -501,6 +639,8 @@ abstract class File { * * @param $width Integer: maximum width of the generated thumbnail * @param $height Integer: maximum height of the image (optional) + * + * @return string */ public function createThumb( $width, $height = -1 ) { $params = array( 'width' => $width ); @@ -512,30 +652,6 @@ abstract class File { return $thumb->getUrl(); } - /** - * As createThumb, but returns a ThumbnailImage object. This can - * provide access to the actual file, the real size of the thumb, - * and can produce a convenient \ tag for you. - * - * For non-image formats, this may return a filetype-specific icon. - * - * @param $width Integer: maximum width of the generated thumbnail - * @param $height Integer: maximum height of the image (optional) - * @param $render Integer: Deprecated - * - * @return ThumbnailImage or null on failure - * - * @deprecated use transform() - */ - public function getThumbnail( $width, $height=-1, $render = true ) { - wfDeprecated( __METHOD__ ); - $params = array( 'width' => $width ); - if ( $height != -1 ) { - $params['height'] = $height; - } - return $this->transform( $params, 0 ); - } - /** * Transform a media file * @@ -558,7 +674,7 @@ abstract class File { // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791. $descriptionUrl = $this->getDescriptionUrl(); if ( $descriptionUrl ) { - $params['descriptionUrl'] = $wgServer . $descriptionUrl; + $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL ); } $script = $this->getTransformScript(); @@ -586,8 +702,8 @@ abstract class File { if ( file_exists( $thumbPath )) { $thumbTime = filemtime( $thumbPath ); if ( $thumbTime !== FALSE && - gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) { - + gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) { + $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); break; } @@ -603,9 +719,9 @@ abstract class File { $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); } } - - // Purge. Useful in the event of Core -> Squid connection failure or squid - // purge collisions from elsewhere during failure. Don't keep triggering for + + // 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 && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) { SquidUpdate::purge( array( $thumbUrl ) ); @@ -625,6 +741,7 @@ abstract class File { /** * Get a MediaHandler instance for this file + * @return MediaHandler */ function getHandler() { if ( !isset( $this->handler ) ) { @@ -664,7 +781,9 @@ abstract class File { * STUB * Overridden by LocalFile */ - function getThumbnails() { return array(); } + function getThumbnails() { + return array(); + } /** * Purge shared caches such as thumbnails and DB data caching @@ -711,6 +830,8 @@ abstract class File { * @param $start timestamp Only revisions older than $start will be returned * @param $end timestamp Only revisions newer than $end will be returned * @param $inc bool Include the endpoints of the time range + * + * @return array */ function getHistory($limit = null, $start = null, $end = null, $inc=true) { return array(); @@ -740,6 +861,8 @@ abstract class File { * Get the filename hash component of the directory including trailing slash, * e.g. f/fa/ * If the repository is not hashed, returns an empty string. + * + * @return string */ function getHashPath() { if ( !isset( $this->hashPath ) ) { @@ -750,6 +873,8 @@ abstract class File { /** * Get the path of the file relative to the public zone root + * + * @return string */ function getRel() { return $this->getHashPath() . $this->getName(); @@ -757,12 +882,20 @@ abstract class File { /** * Get urlencoded relative path of the file + * + * @return string */ function getUrlRel() { return $this->getHashPath() . rawurlencode( $this->getName() ); } - /** Get the relative path for an archive file */ + /** + * Get the relative path for an archived file + * + * @param $suffix bool|string if not false, the name of an archived thumbnail file + * + * @return string + */ function getArchiveRel( $suffix = false ) { $path = 'archive/' . $this->getHashPath(); if ( $suffix === false ) { @@ -773,21 +906,68 @@ abstract class File { return $path; } - /** Get the path of the archive directory, or a particular file if $suffix is specified */ + /** + * Get the relative path for an archived file's thumbs directory + * or a specific thumb if the $suffix is given. + * + * @param $archiveName string the timestamped name of an archived image + * @param $suffix bool|string if not false, the name of a thumbnail file + */ + function getArchiveThumbRel( $archiveName, $suffix = false ) { + $path = 'archive/' . $this->getHashPath() . $archiveName . "/"; + if ( $suffix === false ) { + $path = substr( $path, 0, -1 ); + } else { + $path .= $suffix; + } + return $path; + } + + /** + * Get the path of the archived file. + * + * @param $suffix bool|string if not false, the name of an archived file. + * + * @return string + */ function getArchivePath( $suffix = false ) { - return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix ); + return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix ); } - /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ + /** + * Get the path of the archived file's thumbs, or a particular thumb if $suffix is specified + * + * @param $archiveName string the timestamped name of an archived image + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ + function getArchiveThumbPath( $archiveName, $suffix = false ) { + return $this->repo->getZonePath( 'thumb' ) . '/' . $this->getArchiveThumbRel( $archiveName, $suffix ); + } + + /** + * Get the path of the thumbnail directory, or a particular file if $suffix is specified + * + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ function getThumbPath( $suffix = false ) { - $path = $this->repo->getZonePath('thumb') . '/' . $this->getRel(); + $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getRel(); if ( $suffix !== false ) { $path .= '/' . $suffix; } return $path; } - /** Get the URL of the archive directory, or a particular file if $suffix is specified */ + /** + * Get the URL of the archive directory, or a particular file if $suffix is specified + * + * @param $suffix bool|string if not false, the name of an archived file + * + * @return string + */ function getArchiveUrl( $suffix = false ) { $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath(); if ( $suffix === false ) { @@ -798,7 +978,31 @@ abstract class File { return $path; } - /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */ + /** + * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified + * + * @param $archiveName string the timestamped name of an archived image + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ + function getArchiveThumbUrl( $archiveName, $suffix = false ) { + $path = $this->repo->getZoneUrl('thumb') . '/archive/' . $this->getHashPath() . rawurlencode( $archiveName ) . "/"; + if ( $suffix === false ) { + $path = substr( $path, 0, -1 ); + } else { + $path .= rawurlencode( $suffix ); + } + return $path; + } + + /** + * Get the URL of the thumbnail directory, or a particular file if $suffix is specified + * + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return path + */ function getThumbUrl( $suffix = false ) { $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel(); if ( $suffix !== false ) { @@ -807,7 +1011,13 @@ abstract class File { return $path; } - /** Get the virtual URL for an archive file or directory */ + /** + * Get the virtual URL for an archived file's thumbs, or a specific thumb. + * + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ function getArchiveVirtualUrl( $suffix = false ) { $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath(); if ( $suffix === false ) { @@ -818,7 +1028,13 @@ abstract class File { return $path; } - /** Get the virtual URL for a thumbnail file or directory */ + /** + * Get the virtual URL for a thumbnail file or directory + * + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ function getThumbVirtualUrl( $suffix = false ) { $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel(); if ( $suffix !== false ) { @@ -827,7 +1043,13 @@ abstract class File { return $path; } - /** Get the virtual URL for the file itself */ + /** + * Get the virtual URL for the file itself + * + * @param $suffix bool|string if not false, the name of a thumbnail file + * + * @return string + */ function getVirtualUrl( $suffix = false ) { $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel(); if ( $suffix !== false ) { @@ -843,6 +1065,9 @@ abstract class File { return $this->repo->isHashed(); } + /** + * @throws MWException + */ function readOnlyError() { throw new MWException( get_class($this) . ': write operations are not supported' ); } @@ -851,6 +1076,12 @@ 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 */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { $this->readOnlyError(); @@ -879,46 +1110,8 @@ abstract class File { } /** - * Get an array of Title objects which are articles which use this file - * Also adds their IDs to the link cache - * - * This is mostly copied from Title::getLinksTo() - * - * @deprecated Use HTMLCacheUpdate, this function uses too much memory + * @return bool */ - function getLinksTo( $options = array() ) { - wfDeprecated( __METHOD__ ); - wfProfileIn( __METHOD__ ); - - // Note: use local DB not repo DB, we want to know local links - if ( count( $options ) > 0 ) { - $db = wfGetDB( DB_MASTER ); - } else { - $db = wfGetDB( DB_SLAVE ); - } - $linkCache = LinkCache::singleton(); - - $encName = $db->addQuotes( $this->getName() ); - $res = $db->select( array( 'page', 'imagelinks'), - array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), - array( 'page_id=il_from', 'il_to' => $encName ), - __METHOD__, - $options ); - - $retVal = array(); - if ( $db->numRows( $res ) ) { - foreach ( $res as $row ) { - $titleObj = Title::newFromRow( $row ); - if ( $titleObj ) { - $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest ); - $retVal[] = $titleObj; - } - } - } - wfProfileOut( __METHOD__ ); - return $retVal; - } - function formatMetadata() { if ( !$this->getHandler() ) { return false; @@ -944,8 +1137,11 @@ abstract class File { function getRepoName() { return $this->repo ? $this->repo->getName() : 'unknown'; } - /* + + /** * Returns the repository + * + * @return FileRepo */ function getRepo() { return $this->repo; @@ -954,6 +1150,8 @@ abstract class File { /** * Returns true if the image is an old version * STUB + * + * @return bool */ function isOld() { return false; @@ -962,15 +1160,19 @@ abstract class File { /** * Is this file a "deleted" file in a private archive? * STUB + * + * @param $field + * + * @return bool */ function isDeleted( $field ) { return false; } - + /** * Return the deletion bitfield * STUB - */ + */ function getVisibility() { return 0; } @@ -1025,21 +1227,21 @@ abstract class File { * * May throw database exceptions on error. * - * @param $versions set of record ids of deleted items to restore, + * @param $versions array set of record ids of deleted items to restore, * or empty to restore all revisions. - * @param $unsuppress remove restrictions on content upon restoration? - * @return the number of file revisions restored if successful, + * @param $unsuppress bool remove restrictions on content upon restoration? + * @return int|false the number of file revisions restored if successful, * or false on failure * STUB * Overridden by LocalFile */ - function restore( $versions=array(), $unsuppress=false ) { + function restore( $versions = array(), $unsuppress = false ) { $this->readOnlyError(); } /** - * Returns 'true' if this file is a type which supports multiple pages, - * e.g. DJVU or PDF. Note that this may be true even if the file in + * Returns 'true' if this file is a type which supports multiple pages, + * e.g. DJVU or PDF. Note that this may be true even if the file in * question only has a single page. * * @return Bool @@ -1051,6 +1253,8 @@ abstract class File { /** * Returns the number of pages of a multipage document, or false for * documents which aren't multipage documents + * + * @return false|int */ function pageCount() { if ( !isset( $this->pageCount ) ) { @@ -1065,6 +1269,12 @@ abstract class File { /** * Calculate the height of a thumbnail using the source and destination width + * + * @param $srcWidth + * @param $srcHeight + * @param $dstWidth + * + * @return int */ static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) { // Exact integer multiply followed by division @@ -1092,6 +1302,8 @@ abstract class File { /** * Get the URL of the image description page. May return false if it is * unknown or not applicable. + * + * @return string */ function getDescriptionUrl() { return $this->repo->getDescriptionUrl( $this->getName() ); @@ -1099,6 +1311,8 @@ abstract class File { /** * Get the HTML text of the description page, if available + * + * @return string */ function getDescriptionText() { global $wgMemc, $wgLang; @@ -1109,7 +1323,7 @@ abstract class File { if ( $renderUrl ) { if ( $this->repo->descriptionCacheExpiry > 0 ) { wfDebug("Attempting to get the description from cache..."); - $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(), + $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(), $this->getName() ); $obj = $wgMemc->get($key); if ($obj) { @@ -1132,6 +1346,8 @@ abstract class File { /** * Get discription of file revision * STUB + * + * @return string */ function getDescription() { return null; @@ -1140,6 +1356,8 @@ abstract class File { /** * Get the 14-character timestamp of the file upload, or false if * it doesn't exist + * + * @return string */ function getTimestamp() { $path = $this->getPath(); @@ -1151,6 +1369,8 @@ abstract class File { /** * Get the SHA-1 base 36 hash of the file + * + * @return string */ function getSha1() { return self::sha1Base36( $this->getPath() ); @@ -1158,6 +1378,8 @@ abstract class File { /** * Get the deletion archive key, . + * + * @return string */ function getStorageKey() { $hash = $this->getSha1(); @@ -1166,7 +1388,7 @@ abstract class File { } $ext = $this->getExtension(); $dotExt = $ext === '' ? '' : ".$ext"; - return $hash . $dotExt; + return $hash . $dotExt; } /** @@ -1186,6 +1408,8 @@ abstract class File { * @param $path String: 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 */ static function getPropsFromPath( $path, $ext = true ) { wfProfileIn( __METHOD__ ); @@ -1258,7 +1482,9 @@ abstract class File { * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36 * fairly neatly. * - * Returns false on failure + * @param $path string + * + * @return false|string False on failure */ static function sha1Base36( $path ) { wfSuppressWarnings(); @@ -1271,6 +1497,9 @@ abstract class File { } } + /** + * @return string + */ function getLongDesc() { $handler = $this->getHandler(); if ( $handler ) { @@ -1280,6 +1509,9 @@ abstract class File { } } + /** + * @return string + */ function getShortDesc() { $handler = $this->getHandler(); if ( $handler ) { @@ -1289,6 +1521,9 @@ abstract class File { } } + /** + * @return string + */ function getDimensionsString() { $handler = $this->getHandler(); if ( $handler ) { @@ -1298,22 +1533,36 @@ abstract class File { } } + /** + * @return + */ function getRedirected() { return $this->redirected; } - + + /** + * @return Title + */ function getRedirectedTitle() { if ( $this->redirected ) { - if ( !$this->redirectTitle ) + if ( !$this->redirectTitle ) { $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected ); + } return $this->redirectTitle; } } + /** + * @param $from + * @return void + */ function redirectedFrom( $from ) { $this->redirected = $from; } + /** + * @return bool + */ function isMissing() { return false; } diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index ff73a73c..843f09a9 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -17,6 +17,7 @@ abstract class FileRepo { const DELETE_SOURCE = 1; const OVERWRITE = 2; const OVERWRITE_SAME = 4; + const SKIP_VALIDATION = 8; var $thumbScriptUrl, $transformVia404; var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; @@ -39,7 +40,7 @@ abstract class FileRepo { $this->initialCapital = MWNamespace::isCapitalized( NS_FILE ); foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', - 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl', 'scriptExtension' ) + 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl', 'scriptExtension' ) as $var ) { if ( isset( $info[$var] ) ) { @@ -51,6 +52,10 @@ abstract class FileRepo { /** * Determine if a string is an mwrepo:// URL + * + * @param $url string + * + * @return bool */ static function isVirtualUrl( $url ) { return substr( $url, 0, 9 ) == 'mwrepo://'; @@ -65,9 +70,11 @@ abstract class FileRepo { * 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 */ function newFile( $title, $time = false ) { - if ( !($title instanceof Title) ) { + if ( !( $title instanceof Title ) ) { $title = Title::makeTitleSafe( NS_FILE, $title ); if ( !is_object( $title ) ) { return null; @@ -90,7 +97,7 @@ abstract class FileRepo { * version control should return false if the time is specified. * * @param $title Mixed: Title object or string - * @param $options Associative array of options: + * @param $options array 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. @@ -100,14 +107,11 @@ abstract class FileRepo { * private: If true, return restricted (deleted) files if the current * user is allowed to view them. Otherwise, such files will not * be found. + * + * @return File|false */ function findFile( $title, $options = array() ) { - if ( !is_array( $options ) ) { - // MW 1.15 compat - $time = $options; - } else { - $time = isset( $options['time'] ) ? $options['time'] : false; - } + $time = isset( $options['time'] ) ? $options['time'] : false; if ( !($title instanceof Title) ) { $title = Title::makeTitleSafe( NS_FILE, $title ); if ( !is_object( $title ) ) { @@ -126,9 +130,9 @@ abstract class FileRepo { if ( $time !== false ) { $img = $this->newFile( $title, $time ); if ( $img && $img->exists() ) { - if ( !$img->isDeleted(File::DELETED_FILE) ) { - return $img; - } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) { + if ( !$img->isDeleted( File::DELETED_FILE ) ) { + return $img; // always OK + } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { return $img; } } @@ -139,7 +143,7 @@ abstract class FileRepo { return false; } $redir = $this->checkRedirect( $title ); - if( $redir && $redir->getNamespace() == NS_FILE) { + if( $redir && $title->getNamespace() == NS_FILE) { $img = $this->newFile( $redir ); if( !$img ) { return false; @@ -152,7 +156,7 @@ abstract class FileRepo { return false; } - /* + /** * Find many files at once. * @param $items An array of titles, or an array of findFile() options with * the "title" option giving the title. Example: @@ -180,59 +184,33 @@ abstract class FileRepo { return $result; } - /** - * Create a new File object from the local repository - * @param $sha1 Mixed: SHA-1 key - * @param $time Mixed: time at which the image was uploaded. - * If this is specified, the returned object will be an - * 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. - */ - function newFileFromKey( $sha1, $time = false ) { - if ( $time ) { - if ( $this->oldFileFactoryKey ) { - return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); - } - } else { - if ( $this->fileFactoryKey ) { - return call_user_func( $this->fileFactoryKey, $sha1, $this ); - } - } - return false; - } - /** * Find an instance of the file with this key, created at the specified time * Returns false if the file does not exist. Repositories not supporting * version control should return false if the time is specified. * - * @param $sha1 String + * @param $sha1 String base 36 SHA-1 hash * @param $options Option array, same as findFile(). */ function findFileFromKey( $sha1, $options = array() ) { - if ( !is_array( $options ) ) { - # MW 1.15 compat - $time = $options; - } else { - $time = isset( $options['time'] ) ? $options['time'] : false; - } + $time = isset( $options['time'] ) ? $options['time'] : false; - # First try the current version of the file to see if it precedes the timestamp - $img = $this->newFileFromKey( $sha1 ); - if ( !$img ) { - return false; + # First try to find a matching current version of a file... + if ( $this->fileFactoryKey ) { + $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time ); + } else { + return false; // find-by-sha1 not supported } - if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { + if ( $img && $img->exists() ) { return $img; } - # Now try an old version of the file - if ( $time !== false ) { - $img = $this->newFileFromKey( $sha1, $time ); + # Now try to find a matching old version of a file... + if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported? + $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); if ( $img && $img->exists() ) { - if ( !$img->isDeleted(File::DELETED_FILE) ) { - return $img; - } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) { + if ( !$img->isDeleted( File::DELETED_FILE ) ) { + return $img; // always OK + } elseif ( !empty( $options['private'] ) && $img->userCan( File::DELETED_FILE ) ) { return $img; } } @@ -265,6 +243,7 @@ abstract class FileRepo { /** * Get the name of an image from its title object + * @param $title Title */ function getNameFromTitle( $title ) { if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { @@ -306,18 +285,18 @@ abstract class FileRepo { function getName() { return $this->name; } - + /** * Make an url to this repo - * + * * @param $query mixed Query string to append * @param $entry string Entry point; defaults to index * @return string */ function makeUrl( $query = '', $entry = 'index' ) { $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; - return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); - } + return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); + } /** * Get the URL of an image description page. May return false if it is @@ -367,7 +346,7 @@ abstract class FileRepo { $query .= '&uselang=' . $lang; } if ( isset( $this->scriptDirUrl ) ) { - return $this->makeUrl( + return $this->makeUrl( 'title=' . wfUrlencode( 'Image:' . $name ) . "&$query" ); @@ -380,7 +359,7 @@ abstract class FileRepo { } } } - + /** * Get the URL of the stylesheet to apply to description pages * @return string @@ -433,7 +412,8 @@ abstract class FileRepo { /** - * Append the contents of the source path to the given file. + * Append the contents of the source path to the given file, OR queue + * the appending operation in anticipation of a later appendFinish() call. * @param $srcPath String: location of the source file * @param $toAppendPath String: path to append to. * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate @@ -442,6 +422,13 @@ abstract class FileRepo { */ abstract function append( $srcPath, $toAppendPath, $flags = 0 ); + /** + * Finish the append operation. + * @param $toAppendPath String: path to append to. + * @return mixed Status or false + */ + abstract function appendFinish( $toAppendPath ); + /** * Remove a temporary file or mark it for garbage collection * @param $virtualUrl String: the virtual URL returned by storeTemp @@ -602,7 +589,7 @@ abstract class FileRepo { function newFatal( $message /*, parameters...*/ ) { $params = func_get_args(); array_unshift( $params, $this ); - return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params ); + return MWInit::callStaticMethod( 'FileRepoStatus', 'newFatal', $params ); } /** @@ -624,6 +611,7 @@ abstract class FileRepo { * STUB * * @param $title Title of image + * @return Bool */ function checkRedirect( $title ) { return false; @@ -658,11 +646,7 @@ abstract class FileRepo { return null; } // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true - $repoName = wfMsg( 'shared-repo-name-' . $this->name ); - if ( !wfEmptyMsg( 'shared-repo-name-' . $this->name, $repoName ) ) { - return $repoName; - } - return wfMsg( 'shared-repo' ); + return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text(); } /** @@ -696,9 +680,11 @@ abstract class FileRepo { array_unshift( $args, 'filerepo', $this->getName() ); return call_user_func_array( 'wfMemcKey', $args ); } - + /** * Get an UploadStash associated with this repo. + * + * @return UploadStash */ function getUploadStash() { return new UploadStash( $this ); diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 161284c0..4eea9030 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -13,6 +13,10 @@ class FileRepoStatus extends Status { /** * Factory function for fatal errors + * + * @param $repo FileRepo + * + * @return FileRepoStatus */ static function newFatal( $repo /*, parameters...*/ ) { $params = array_slice( func_get_args(), 1 ); @@ -22,12 +26,20 @@ class FileRepoStatus extends Status { return $result; } + /** + * @param $repo FileRepo + * @param $value + * @return FileRepoStatus + */ static function newGood( $repo = false, $value = null ) { $result = new self( $repo ); $result->value = $value; return $result; } + /** + * @param $repo FileRepo + */ function __construct( $repo = false ) { if ( $repo ) { $this->cleanCallback = $repo->getErrorCleanupFunction(); diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php index 56fed75e..53c4a3bd 100644 --- a/includes/filerepo/ForeignAPIFile.php +++ b/includes/filerepo/ForeignAPIFile.php @@ -15,7 +15,13 @@ class ForeignAPIFile extends File { private $mExists; - + + /** + * @param $title + * @param $repo ForeignApiRepo + * @param $info + * @param bool $exists + */ function __construct( $title, $repo, $info, $exists = false ) { parent::__construct( $title, $repo ); $this->mInfo = $info; @@ -23,7 +29,6 @@ class ForeignAPIFile extends File { } /** - * @static * @param $title Title * @param $repo ForeignApiRepo * @return ForeignAPIFile|null @@ -32,7 +37,9 @@ class ForeignAPIFile extends File { $data = $repo->fetchImageQuery( array( 'titles' => 'File:' . $title->getDBKey(), 'iiprop' => self::getProps(), - 'prop' => 'imageinfo' ) ); + 'prop' => 'imageinfo', + 'iimetadataversion' => MediaHandler::getMetadataVersion() + ) ); $info = $repo->getImageInfo( $data ); @@ -76,20 +83,26 @@ class ForeignAPIFile extends File { // show icon return parent::transform( $params, $flags ); } + + // Note, the this->canRender() check above implies + // that we have a handler, and it can do makeParamString. + $otherParams = $this->handler->makeParamString( $params ); + $thumbUrl = $this->repo->getThumbUrlFromCache( $this->getName(), isset( $params['width'] ) ? $params['width'] : -1, - isset( $params['height'] ) ? $params['height'] : -1 ); + isset( $params['height'] ) ? $params['height'] : -1, + $otherParams ); return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params ); } // Info we can get from API... public function getWidth( $page = 1 ) { - return intval( @$this->mInfo['width'] ); + return isset( $this->mInfo['width'] ) ? intval( $this->mInfo['width'] ) : 0; } public function getHeight( $page = 1 ) { - return intval( @$this->mInfo['height'] ); + return isset( $this->mInfo['height'] ) ? intval( $this->mInfo['height'] ) : 0; } public function getMetadata() { @@ -148,7 +161,7 @@ class ForeignAPIFile extends File { return $this->mInfo['mime']; } - /// @todo Fixme: may guess wrong on file types that can be eg audio or video + /// @todo FIXME: May guess wrong on file types that can be eg audio or video function getMediaType() { $magic = MimeMagic::singleton(); return $magic->getMediaType( null, $this->getMimeType() ); @@ -182,7 +195,7 @@ class ForeignAPIFile extends File { $handle = opendir( $dir ); if ( $handle ) { while ( false !== ( $file = readdir($handle) ) ) { - if ( $file{0} != '.' ) { + if ( $file[0] != '.' ) { $files[] = $file; } } diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index e4188d6b..502b8c1d 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -25,13 +25,13 @@ class ForeignAPIRepo extends FileRepo { /* This version string is used in the user agent for requests and will help * server maintainers in identify ForeignAPI usage. * Update the version every time you make breaking or significant changes. */ - const VERSION = "2.0"; + const VERSION = "2.1"; var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); /* Check back with Commons after a day */ - var $apiThumbCacheExpiry = 86400; + var $apiThumbCacheExpiry = 86400; /* 24*60*60 */ /* Redownload thumbnail files after a month */ - var $fileCacheExpiry = 2629743; + var $fileCacheExpiry = 2592000; /* 86400*30 */ /* Local image directory */ var $directory; var $thumbDir; @@ -75,6 +75,8 @@ class ForeignAPIRepo extends FileRepo { /** * Per docs in FileRepo, this needs to return false if we don't support versioned * files. Well, we don't. + * + * @return File */ function newFile( $title, $time = false ) { if ( $time ) { @@ -83,26 +85,34 @@ class ForeignAPIRepo extends FileRepo { return parent::newFile( $title, $time ); } -/** - * No-ops - */ + /** + * No-ops + */ + function storeBatch( $triplets, $flags = 0 ) { return false; } + function storeTemp( $originalName, $srcPath ) { return false; } + function append( $srcPath, $toAppendPath, $flags = 0 ){ return false; } + + function appendFinish( $toAppendPath ){ + return false; + } + function publishBatch( $triplets, $flags = 0 ) { return false; } + function deleteBatch( $sourceDestPairs ) { return false; } - function fileExistsBatch( $files, $flags = 0 ) { $results = array(); foreach ( $files as $k => $f ) { @@ -110,7 +120,7 @@ class ForeignAPIRepo extends FileRepo { $results[$k] = true; unset( $files[$k] ); } elseif( self::isVirtualUrl( $f ) ) { - # TODO! FIXME! We need to be able to handle virtual + # @todo FIXME: We need to be able to handle virtual # URLs better, at least when we know they refer to the # same repo. $results[$k] = false; @@ -129,6 +139,7 @@ class ForeignAPIRepo extends FileRepo { } return $results; } + function getFileProps( $virtualUrl ) { return false; } @@ -197,12 +208,13 @@ class ForeignAPIRepo extends FileRepo { return $ret; } - function getThumbUrl( $name, $width=-1, $height=-1, &$result=NULL ) { + function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) { $data = $this->fetchImageQuery( array( 'titles' => 'File:' . $name, 'iiprop' => 'url|timestamp', 'iiurlwidth' => $width, 'iiurlheight' => $height, + 'iiurlparam' => $otherParams, 'prop' => 'imageinfo' ) ); $info = $this->getImageInfo( $data ); @@ -215,7 +227,7 @@ class ForeignAPIRepo extends FileRepo { } } - /* + /** * Return the imageurl from cache if possible * * If the url has been requested today, get it from cache @@ -224,15 +236,17 @@ class ForeignAPIRepo extends FileRepo { * @param $name String is a dbkey form of a title * @param $width * @param $height + * @param String $param Other rendering parameters (page number, etc) from handler's makeParamString. */ - function getThumbUrlFromCache( $name, $width, $height ) { + function getThumbUrlFromCache( $name, $width, $height, $params="" ) { global $wgMemc; if ( !$this->canCacheThumbs() ) { - return $this->getThumbUrl( $name, $width, $height ); + $result = null; // can't pass "null" by reference, but it's ok as default value + return $this->getThumbUrl( $name, $width, $height, $result, $params ); } $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $name ); - $sizekey = "$width:$height"; + $sizekey = "$width:$height:$params"; /* Get the array of urls that we already know */ $knownThumbUrls = $wgMemc->get($key); @@ -241,14 +255,15 @@ class ForeignAPIRepo extends FileRepo { $knownThumbUrls = array(); } else { if( isset( $knownThumbUrls[$sizekey] ) ) { - wfDebug("Got thumburl from local cache. {$knownThumbUrls[$sizekey]} \n"); + wfDebug( __METHOD__ . ': Got thumburl from local cache: ' . + "{$knownThumbUrls[$sizekey]} \n"); return $knownThumbUrls[$sizekey]; } /* This size is not yet known */ } $metadata = null; - $foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata ); + $foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata, $params ); if( !$foreignUrl ) { wfDebug( __METHOD__ . " Could not find thumburl\n" ); @@ -273,7 +288,7 @@ class ForeignAPIRepo extends FileRepo { $diff = abs( $modified - $current ); if( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) { /* Use our current and already downloaded thumbnail */ - $knownThumbUrls["$width:$height"] = $localUrl; + $knownThumbUrls[$sizekey] = $localUrl; $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); return $localUrl; } @@ -291,7 +306,7 @@ class ForeignAPIRepo extends FileRepo { } } - # FIXME: Delete old thumbs that aren't being used. Maintenance script? + # @todo FIXME: Delete old thumbs that aren't being used. Maintenance script? wfSuppressWarnings(); if( !file_put_contents( $localFilename, $thumb ) ) { wfRestoreWarnings(); @@ -355,7 +370,7 @@ class ForeignAPIRepo extends FileRepo { public static function httpGet( $url, $timeout = 'default', $options = array() ) { $options['timeout'] = $timeout; /* Http::get */ - $url = wfExpandUrl( $url ); + $url = wfExpandUrl( $url, PROTO_HTTP ); wfDebug( "ForeignAPIRepo: HTTP GET: $url\n" ); $options['method'] = "GET"; diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php index 5f04ea73..09bee39c 100644 --- a/includes/filerepo/ForeignDBFile.php +++ b/includes/filerepo/ForeignDBFile.php @@ -12,6 +12,13 @@ * @ingroup FileRepo */ class ForeignDBFile extends LocalFile { + + /** + * @param $title + * @param $repo + * @param $unused + * @return ForeignDBFile + */ static function newFromTitle( $title, $repo, $unused = null ) { return new self( $title, $repo ); } @@ -19,6 +26,11 @@ class ForeignDBFile extends LocalFile { /** * Create a ForeignDBFile from a title * Do not call this except from inside a repo class. + * + * @param $row + * @param $repo + * + * @return ForeignDBFile */ static function newFromRow( $row, $repo ) { $title = Title::makeTitle( NS_FILE, $row->img_name ); @@ -35,21 +47,30 @@ class ForeignDBFile extends LocalFile { $watch = false, $timestamp = false ) { $this->readOnlyError(); } + function restore( $versions = array(), $unsuppress = false ) { $this->readOnlyError(); } + function delete( $reason, $suppress = false ) { $this->readOnlyError(); } + function move( $target ) { $this->readOnlyError(); } - + + /** + * @return string + */ function getDescriptionUrl() { // Restore remote behaviour return File::getDescriptionUrl(); } + /** + * @return string + */ function getDescriptionText() { // Restore remote behaviour return File::getDescriptionText(); diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index a756703f..0311ebcd 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -35,14 +35,14 @@ class ForeignDBRepo extends LocalRepo { function getMasterDB() { if ( !isset( $this->dbConn ) ) { - $this->dbConn = DatabaseBase::newFromType( $this->dbType, + $this->dbConn = DatabaseBase::factory( $this->dbType, array( 'host' => $this->dbServer, 'user' => $this->dbUser, 'password' => $this->dbPassword, 'dbname' => $this->dbName, 'flags' => $this->dbFlags, - 'tableprefix' => $this->tablePrefix + 'tablePrefix' => $this->tablePrefix ) ); } diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php deleted file mode 100644 index 59a07ef9..00000000 --- a/includes/filerepo/Image.php +++ /dev/null @@ -1,80 +0,0 @@ -getLocalRepo(); - parent::__construct( $title, $repo ); - } - - /** - * Wrapper for wfFindFile(), for backwards-compatibility only - * Do not use in core code. - * @deprecated - */ - static function newFromTitle( $title, $repo, $time = null ) { - wfDeprecated( __METHOD__ ); - $img = wfFindFile( $title, array( 'time' => $time ) ); - if ( !$img ) { - $img = wfLocalFile( $title ); - } - return $img; - } - - /** - * Wrapper for wfFindFile(), for backwards-compatibility only. - * Do not use in core code. - * - * @param $name String: name of the image, used to create a title object using Title::makeTitleSafe - * @return image object or null if invalid title - * @deprecated - */ - static function newFromName( $name ) { - wfDeprecated( __METHOD__ ); - $title = Title::makeTitleSafe( NS_FILE, $name ); - if ( is_object( $title ) ) { - $img = wfFindFile( $title ); - if ( !$img ) { - $img = wfLocalFile( $title ); - } - return $img; - } else { - return null; - } - } - - /** - * Return the URL of an image, provided its name. - * - * Backwards-compatibility for extensions. - * Note that fromSharedDirectory will only use the shared path for files - * that actually exist there now, and will return local paths otherwise. - * - * @param $name String: name of the image, without the leading "Image:" - * @param $fromSharedDirectory Boolean: Should this be in $wgSharedUploadPath? - * @return string URL of $name image - * @deprecated - */ - static function imageUrl( $name, $fromSharedDirectory = false ) { - wfDeprecated( __METHOD__ ); - $image = null; - if( $fromSharedDirectory ) { - $image = wfFindFile( $name ); - } - if( !$image ) { - $image = wfLocalFile( $name ); - } - return $image->getUrl(); - } -} diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index 5489ecb2..14da9122 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -33,7 +33,7 @@ class LocalFile extends File { * @private */ var - $fileExists, # does the file file exist on disk? (loadFromXxx) + $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, # \ @@ -63,6 +63,12 @@ class LocalFile extends File { * Do not call this except from inside a repo class. * * Note: $unused param is only here to avoid an E_STRICT + * + * @param $title + * @param $repo + * @param $unused + * + * @return LocalFile */ static function newFromTitle( $title, $repo, $unused = null ) { return new self( $title, $repo ); @@ -71,6 +77,11 @@ class LocalFile extends File { /** * Create a LocalFile from a title * Do not call this except from inside a repo class. + * + * @param $row + * @param $repo + * + * @return LocalFile */ static function newFromRow( $row, $repo ) { $title = Title::makeTitle( NS_FILE, $row->img_name ); @@ -83,17 +94,22 @@ class LocalFile extends File { /** * Create a LocalFile from a SHA-1 key * Do not call this except from inside a repo class. + * + * @param $sha1 string base-36 SHA-1 + * @param $repo LocalRepo + * @param string|bool $timestamp MW_timestamp (optional) + * + * @return bool|LocalFile */ static function newFromKey( $sha1, $repo, $timestamp = false ) { - $conds = array( 'img_sha1' => $sha1 ); + $dbr = $repo->getSlaveDB(); + $conds = array( 'img_sha1' => $sha1 ); if ( $timestamp ) { - $conds['img_timestamp'] = $timestamp; + $conds['img_timestamp'] = $dbr->timestamp( $timestamp ); } - $dbr = $repo->getSlaveDB(); $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ ); - if ( $row ) { return self::newFromRow( $row, $repo ); } else { @@ -333,6 +349,7 @@ class LocalFile extends File { * Upgrade a row if it needs it */ function maybeUpgradeRow() { + global $wgUpdateCompatibleMetadata; if ( wfReadOnly() ) { return; } @@ -344,9 +361,14 @@ class LocalFile extends File { $this->upgraded = true; } else { $handler = $this->getHandler(); - if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) { - $this->upgradeRow(); - $this->upgraded = true; + if ( $handler ) { + $validity = $handler->isMetadataValid( $this, $this->metadata ); + if ( $validity === MediaHandler::METADATA_BAD + || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata ) + ) { + $this->upgradeRow(); + $this->upgraded = true; + } } } } @@ -540,8 +562,8 @@ class LocalFile extends File { /** isTrustedFile inherited */ /** - * Returns true if the file file exists on disk. - * @return boolean Whether file file exist on disk. + * Returns true if the file exists on disk. + * @return boolean Whether file exist on disk. */ public function exists() { $this->load(); @@ -552,7 +574,6 @@ class LocalFile extends File { /** getUnscaledThumb inherited */ /** thumbName inherited */ /** createThumb inherited */ - /** getThumbnail inherited */ /** transform inherited */ /** @@ -591,12 +612,19 @@ class LocalFile extends File { /** * Get all thumbnail names previously generated for this file + * @param $archiveName string|false Name of an archive file + * @return array first element is the base dir, then files in that base dir. */ - function getThumbnails() { + function getThumbnails( $archiveName = false ) { $this->load(); + if ( $archiveName ) { + $dir = $this->getArchiveThumbPath( $archiveName ); + } else { + $dir = $this->getThumbPath(); + } $files = array(); - $dir = $this->getThumbPath(); + $files[] = $dir; if ( is_dir( $dir ) ) { $handle = opendir( $dir ); @@ -633,6 +661,11 @@ class LocalFile extends File { $hashedName = md5( $this->getName() ); $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName ); + // Must purge thumbnails for old versions too! bug 30192 + foreach( $this->getHistory() as $oldFile ) { + $oldFile->purgeThumbnails(); + } + if ( $oldKey ) { $wgMemc->delete( $oldKey ); } @@ -653,32 +686,76 @@ class LocalFile extends File { } /** - * Delete cached transformed files + * Delete cached transformed files for archived files + * @param $archiveName string name of the archived file */ - function purgeThumbnails() { + function purgeOldThumbnails( $archiveName ) { global $wgUseSquid; + // get a list of old thumbnails and URLs + $files = $this->getThumbnails( $archiveName ); + $dir = array_shift( $files ); + $this->purgeThumbList( $dir, $files ); - // Delete thumbnails - $files = $this->getThumbnails(); - $dir = $this->getThumbPath(); - $urls = array(); + // Directory should be empty, delete it too. This will probably suck on + // something like NFS or if the directory isn't actually empty, so hide + // the warnings :D + wfSuppressWarnings(); + if( !rmdir( $dir ) ) { + wfDebug( __METHOD__ . ": unable to remove archive directory: $dir\n" ); + } + wfRestoreWarnings(); - foreach ( $files as $file ) { - # Check that the base file name is part of the thumb name - # This is a basic sanity check to avoid erasing unrelated directories - if ( strpos( $file, $this->getName() ) !== false ) { - $url = $this->getThumbUrl( $file ); - $urls[] = $url; - @unlink( "$dir/$file" ); + // Purge the squid + if ( $wgUseSquid ) { + $urls = array(); + foreach( $files as $file ) { + $urls[] = $this->getArchiveThumbUrl( $archiveName, $file ); } + SquidUpdate::purge( $urls ); } + } + + + /** + * Delete cached transformed files for the current version only. + */ + function purgeThumbnails() { + global $wgUseSquid; + // get a list of thumbnails and URLs + $files = $this->getThumbnails(); + $dir = array_shift( $files ); + $this->purgeThumbList( $dir, $files ); // Purge the squid if ( $wgUseSquid ) { + $urls = array(); + foreach( $files as $file ) { + $urls[] = $this->getThumbUrl( $file ); + } SquidUpdate::purge( $urls ); } } + /** + * Delete a list of thumbnails visible at urls + * @param $dir string base dir of the files. + * @param $files array of strings: relative filenames (to $dir) + */ + function purgeThumbList($dir, $files) { + global $wgExcludeFromThumbnailPurge; + + wfDebug( __METHOD__ . ": " . var_export( $files, true ) . "\n" ); + foreach ( $files as $file ) { + # Check that the base file name is part of the thumb name + # This is a basic sanity check to avoid erasing unrelated directories + if ( strpos( $file, $this->getName() ) !== false ) { + wfSuppressWarnings(); + unlink( "$dir/$file" ); + wfRestoreWarnings(); + } + } + } + /** purgeDescription inherited */ /** purgeEverything inherited */ @@ -786,7 +863,6 @@ class LocalFile extends File { /** getRel inherited */ /** getUrlRel inherited */ /** getArchiveRel inherited */ - /** getThumbRel inherited */ /** getArchivePath inherited */ /** getThumbPath inherited */ /** getArchiveUrl inherited */ @@ -828,7 +904,6 @@ class LocalFile extends File { /** * Record a file upload in the upload log and the image table - * @deprecated use upload() */ function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false, $timestamp = false ) @@ -844,7 +919,6 @@ class LocalFile extends File { $wgUser->addWatch( $this->getTitle() ); } return true; - } /** @@ -883,7 +957,7 @@ class LocalFile extends File { # Fail now if the file isn't there if ( !$this->fileExists ) { - wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" ); + wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" ); return false; } @@ -961,8 +1035,10 @@ class LocalFile extends File { } else { # This is a new file # Update the image count + $dbw->begin(); $site_stats = $dbw->tableName( 'site_stats' ); $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ ); + $dbw->commit(); } $descTitle = $this->getTitle(); @@ -1036,16 +1112,32 @@ class LocalFile extends File { * * @param $srcPath String: 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 + * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy * @return FileRepoStatus object. On success, the value member contains the * archive name, or an empty string if it was a new file. */ function publish( $srcPath, $flags = 0 ) { + return $this->publishTo( $srcPath, $this->getRel(), $flags ); + } + + /** + * Move or copy a file to a specified location. Returns a FileRepoStatus + * object with the archive name in the "value" member on success. + * + * The archive name should be passed through to recordUpload for database + * registration. + * + * @param $srcPath String: local filesystem path to the source image + * @param $dstRel String: target relative path + * @param $flags Integer: a bitwise combination of: + * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy + * @return FileRepoStatus object. 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 ) { $this->lock(); - $dstRel = $this->getRel(); - $archiveName = gmdate( 'YmdHis' ) . '!' . $this->getName(); + $archiveName = wfTimestamp( TS_MW ) . '!'. $this->getName(); $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags ); @@ -1130,6 +1222,7 @@ class LocalFile extends File { array( 'oi_name' => $this->getName() ) ); foreach ( $result as $row ) { $batch->addOld( $row->oi_archive_name ); + $this->purgeOldThumbnails( $row->oi_archive_name ); } $status = $batch->execute(); @@ -1164,6 +1257,7 @@ class LocalFile extends File { $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); $batch->addOld( $archiveName ); + $this->purgeOldThumbnails( $archiveName ); $status = $batch->execute(); $this->unlock(); @@ -1198,7 +1292,7 @@ class LocalFile extends File { $status = $batch->execute(); - if ( !$status->ok ) { + if ( !$status->isGood() ) { return $status; } @@ -1312,7 +1406,13 @@ class LocalFile extends File { * @ingroup FileRepo */ class LocalFileDeleteBatch { - var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress; + + /** + * @var LocalFile + */ + var $file; + + var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress; var $status; function __construct( File $file, $reason = '', $suppress = false ) { @@ -1344,7 +1444,7 @@ class LocalFileDeleteBatch { return array( $oldRels, $deleteCurrent ); } - /*protected*/ function getHashes() { + protected function getHashes() { $hashes = array(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); @@ -1623,7 +1723,12 @@ class LocalFileDeleteBatch { * @ingroup FileRepo */ class LocalFileRestoreBatch { - var $file, $cleanupBatch, $ids, $all, $unsuppress = false; + /** + * @var LocalFile + */ + var $file; + + var $cleanupBatch, $ids, $all, $unsuppress = false; function __construct( File $file, $unsuppress = false ) { $this->file = $file; @@ -1827,9 +1932,11 @@ class LocalFileRestoreBatch { $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); $status->merge( $storeStatus ); - if ( !$status->ok ) { - // Store batch returned a critical error -- this usually means nothing was stored - // Stop now and return an error + if ( !$status->isGood() ) { + // Even if some files could be copied, fail entirely as that is the + // easiest thing to do without data loss + $this->cleanupFailedBatch( $storeStatus, $storeBatch ); + $status->ok = false; $this->file->unlock(); return $status; @@ -1934,6 +2041,27 @@ class LocalFileRestoreBatch { return $status; } + + /** + * Cleanup a failed batch. The batch was only partially successful, so + * rollback by removing all items that were succesfully copied. + * + * @param Status $storeStatus + * @param array $storeBatch + */ + function cleanupFailedBatch( $storeStatus, $storeBatch ) { + $cleanupBatch = array(); + + foreach ( $storeStatus->success as $i => $success ) { + // Check if this item of the batch was successfully copied + if ( $success ) { + // Item was successfully copied and needs to be removed again + // Extract ($dstZone, $dstRel) from the batch + $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] ); + } + } + $this->file->repo->cleanupBatch( $cleanupBatch ); + } } # ------------------------------------------------------------------------------ @@ -1983,14 +2111,14 @@ class LocalFileMoveBatch { $bits = explode( '!', $oldName, 2 ); if ( count( $bits ) != 2 ) { - wfDebug( "Invalid old file name: $oldName \n" ); + wfDebug( "Old file name missing !: '$oldName' \n" ); continue; } list( $timestamp, $filename ) = $bits; if ( $this->oldName != $filename ) { - wfDebug( "Invalid old file name: $oldName \n" ); + wfDebug( "Old file name doesn't match: '$oldName' \n" ); continue; } @@ -2017,15 +2145,31 @@ class LocalFileMoveBatch { $triplets = $this->getMoveTriplets(); $triplets = $this->removeNonexistentFiles( $triplets ); - $statusDb = $this->doDBUpdates(); - wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); - $statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE ); - wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" ); - if ( !$statusMove->isOk() ) { + // Copy the files into their new location + $statusMove = $repo->storeBatch( $triplets ); + wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" ); + if ( !$statusMove->isGood() ) { wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); + $this->cleanupTarget( $triplets ); + $statusMove->ok = false; + return $statusMove; + } + + $this->db->begin(); + $statusDb = $this->doDBUpdates(); + wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" ); + if ( !$statusDb->isGood() ) { $this->db->rollback(); + // Something went wrong with the DB updates, so remove the target files + $this->cleanupTarget( $triplets ); + $statusDb->ok = false; + return $statusDb; } + $this->db->commit(); + + // Everything went ok, remove the source files + $this->cleanupSource( $triplets ); $status->merge( $statusDb ); $status->merge( $statusMove ); @@ -2056,6 +2200,8 @@ class LocalFileMoveBatch { $status->successCount++; } else { $status->failCount++; + $status->fatal( 'imageinvalidfilename' ); + return $status; } // Update old images @@ -2073,6 +2219,9 @@ class LocalFileMoveBatch { $total = $this->oldCount; $status->successCount += $affected; $status->failCount += $total - $affected; + if ( $status->failCount ) { + $status->error( 'imageinvalidfilename' ); + } return $status; } @@ -2117,4 +2266,32 @@ class LocalFileMoveBatch { return $filteredTriplets; } + + /** + * Cleanup a partially moved array of triplets by deleting the target + * files. Called if something went wrong half way. + */ + function cleanupTarget( $triplets ) { + // Create dest pairs from the triplets + $pairs = array(); + foreach ( $triplets as $triplet ) { + $pairs[] = array( $triplet[1], $triplet[2] ); + } + + $this->file->repo->cleanupBatch( $pairs ); + } + + /** + * 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. + */ + function cleanupSource( $triplets ) { + // Create source file names from the triplets + $files = array(); + foreach ( $triplets as $triplet ) { + $files[] = $triplet[0]; + } + + $this->file->repo->cleanupBatch( $files ); + } } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 02883c53..9089f4d7 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -20,6 +20,11 @@ class LocalRepo extends FSRepo { var $fileFromRowFactory = array( 'LocalFile', 'newFromRow' ); var $oldFileFromRowFactory = array( 'OldLocalFile', 'newFromRow' ); + /** + * @throws MWException + * @param $row + * @return File + */ function newFileFromRow( $row ) { if ( isset( $row->img_name ) ) { return call_user_func( $this->fileFromRowFactory, $row, $this ); @@ -30,6 +35,11 @@ class LocalRepo extends FSRepo { } } + /** + * @param $title + * @param $archiveName + * @return OldLocalFile + */ function newFromArchiveName( $title, $archiveName ) { return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); } @@ -39,13 +49,16 @@ class LocalRepo extends FSRepo { * filearchive table. This needs to be done in the repo because it needs to * interleave database locks with file operations, which is potentially a * remote operation. + * + * @param $storageKeys array + * * @return FileRepoStatus */ function cleanupDeletedBatch( $storageKeys ) { $root = $this->getZonePath( 'deleted' ); $dbw = $this->getMasterDB(); $status = $this->newGood(); - $storageKeys = array_unique($storageKeys); + $storageKeys = array_unique( $storageKeys ); foreach ( $storageKeys as $key ) { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; @@ -54,8 +67,8 @@ class LocalRepo extends FSRepo { array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), __METHOD__, array( 'FOR UPDATE' ) ); if( !$inuse ) { - $sha1 = substr( $key, 0, strcspn( $key, '.' ) ); - $ext = substr( $key, strcspn($key,'.') + 1 ); + $sha1 = self::getHashFromKey( $key ); + $ext = substr( $key, strcspn( $key, '.' ) + 1 ); $ext = File::normalizeExtension($ext); $inuse = $dbw->selectField( 'oldimage', '1', array( 'oi_sha1' => $sha1, @@ -65,7 +78,10 @@ class LocalRepo extends FSRepo { } if ( !$inuse ) { wfDebug( __METHOD__ . ": deleting $key\n" ); - if ( !@unlink( $path ) ) { + wfSuppressWarnings(); + $unlink = unlink( $path ); + wfRestoreWarnings(); + if ( !$unlink ) { $status->error( 'undelete-cleanup-error', $path ); $status->failCount++; } @@ -77,6 +93,16 @@ class LocalRepo extends FSRepo { } return $status; } + + /** + * Gets the SHA1 hash from a storage key + * + * @param string $key + * @return string + */ + public static function getHashFromKey( $key ) { + return strtok( $key, '.' ); + } /** * Checks if there is a redirect named as $title @@ -87,7 +113,7 @@ class LocalRepo extends FSRepo { global $wgMemc; if( is_string( $title ) ) { - $title = Title::newFromTitle( $title ); + $title = Title::newFromText( $title ); } if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) { $title = Title::makeTitle( NS_FILE, $title->getText() ); @@ -135,6 +161,7 @@ class LocalRepo extends FSRepo { /** * Function link Title::getArticleID(). * We can't say Title object, what database it should use, so we duplicate that function here. + * @param $title Title */ protected function getArticleID( $title ) { if( !$title instanceof Title ) { diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index d5a1ee03..cac3e5d8 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -23,6 +23,9 @@ class NullRepo extends FileRepo { function append( $srcPath, $toAppendPath, $flags = 0 ){ return false; } + function appendFinish( $toAppendPath ){ + return false; + } function publishBatch( $triplets, $flags = 0 ) { return false; } diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php index 9efe998f..bcb22c17 100644 --- a/includes/filerepo/OldLocalFile.php +++ b/includes/filerepo/OldLocalFile.php @@ -1,6 +1,6 @@ loadFromRow( $row, 'oi_' ); return $file; } - + + /** + * Create a OldLocalFile from a SHA-1 key + * Do not call this except from inside a repo class. + * + * @param $sha1 string base-36 SHA-1 + * @param $repo LocalRepo + * @param string|bool $timestamp MW_timestamp (optional) + * + * @return bool|OldLocalFile + */ static function newFromKey( $sha1, $repo, $timestamp = false ) { + $dbr = $repo->getSlaveDB(); + $conds = array( 'oi_sha1' => $sha1 ); - if( $timestamp ) { - $conds['oi_timestamp'] = $timestamp; + if ( $timestamp ) { + $conds['oi_timestamp'] = $dbr->timestamp( $timestamp ); } - $dbr = $repo->getSlaveDB(); + $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ ); - if( $row ) { + if ( $row ) { return self::newFromRow( $row, $repo ); } else { return false; @@ -205,4 +218,77 @@ class OldLocalFile extends LocalFile { $this->load(); return Revision::userCanBitfield( $this->deleted, $field ); } + + /** + * Upload a file directly into archive. Generally for Special:Import. + * + * @param $srcPath string File system path of the source file + * @param $archiveName string Full archive name of the file, in the form + * $timestamp!$filename, where $filename must match $this->getName() + * + * @return FileRepoStatus + */ + function uploadOld( $srcPath, $archiveName, $timestamp, $comment, $user, $flags = 0 ) { + $this->lock(); + + $dstRel = 'archive/' . $this->getHashPath() . $archiveName; + $status = $this->publishTo( $srcPath, $dstRel, + $flags & File::DELETE_SOURCE ? FileRepo::DELETE_SOURCE : 0 + ); + + if ( $status->isGood() ) { + if ( !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) ) { + $status->fatal( 'filenotfound', $srcPath ); + } + } + + $this->unlock(); + + return $status; + } + + /** + * Record a file upload in the oldimage table, without adding log entries. + * + * @param $srcPath string File system path to the source file + * @param $archiveName string The archive name of the file + * @param $comment string Upload comment + * @param $user User User who did this upload + * @return bool + */ + function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) { + $dbw = $this->repo->getMasterDB(); + $dbw->begin(); + + $dstPath = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel(); + $props = self::getPropsFromPath( $dstPath ); + if ( !$props['fileExists'] ) { + return false; + } + + $dbw->insert( 'oldimage', + array( + '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'], + ), __METHOD__ + ); + + $dbw->commit(); + + return true; + } + } diff --git a/includes/filerepo/README b/includes/filerepo/README index d3aea9f0..db46ff8a 100644 --- a/includes/filerepo/README +++ b/includes/filerepo/README @@ -39,3 +39,21 @@ LocalRepo.php. LocalRepo provides only file access, and LocalFile provides database access and higher-level functions such as cache management. Tim Starling, June 2007 + +Structure: + +File.php defines an abstract class File. + ForeignAPIFile.php extends File. + LocalFile.php extends File. + ForeignDBFile.php extends LocalFile + Image.php extends LocalFile + UnregisteredLocalFile.php extends File. +FileRepo.php defined an abstract class FileRepo. + ForeignAPIRepo.php extends FileRepo + FSRepo extends FileRepo + LocalRepo.php extends FSRepo + ForeignDBRepo.php extends LocalRepo + ForeignDBViaLBRepo.php extends LocalRepo + NullRepo extends FileRepo + +Russ Nelson, March 2011 diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index b9996941..d4875908 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -16,16 +16,26 @@ * @ingroup FileRepo */ class RepoGroup { - var $localRepo, $foreignRepos, $reposInitialised = false; + + /** + * @var LocalRepo + */ + var $localRepo; + + var $foreignRepos, $reposInitialised = false; var $localInfo, $foreignInfo; var $cache; + /** + * @var RepoGroup + */ protected static $instance; const MAX_CACHE_SIZE = 1000; /** * Get a RepoGroup instance. At present only one instance of RepoGroup is * needed in a MediaWiki invocation, this may change in the future. + * @return RepoGroup */ static function singleton() { if ( self::$instance ) { @@ -46,6 +56,8 @@ class RepoGroup { /** * Set the singleton instance to a given object + * + * @param $instance RepoGroup */ static function setSingleton( $instance ) { self::$instance = $instance; @@ -70,8 +82,8 @@ class RepoGroup { * Search repositories for an image. * You can also use wfFindFile() to do this. * - * @param $title Mixed: Title object or string - * @param $options Associative array of options: + * @param $title Title|string Title object or string + * @param $options array 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. @@ -101,7 +113,7 @@ class RepoGroup { } if ( $title->getNamespace() != NS_MEDIA && $title->getNamespace() != NS_FILE ) { - throw new MWException( __METHOD__ . ' recieved an Title object with incorrect namespace' ); + throw new MWException( __METHOD__ . ' received an Title object with incorrect namespace' ); } # Check the cache @@ -204,14 +216,44 @@ class RepoGroup { return false; } + /** + * Find an instance of the file with this key, created at the specified time + * Returns false if the file does not exist. + * + * @param $hash String base 36 SHA-1 hash + * @param $options Option array, same as findFile() + * @return File object or false if it is not found + */ + function findFileFromKey( $hash, $options = array() ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $file = $this->localRepo->findFileFromKey( $hash, $options ); + if ( !$file ) { + foreach ( $this->foreignRepos as $repo ) { + $file = $repo->findFileFromKey( $hash, $options ); + if ( $file ) break; + } + } + return $file; + } + + /** + * Find all instances of files with this key + * + * @param $hash String base 36 SHA-1 hash + * @return Array of File objects + */ function findBySha1( $hash ) { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } $result = $this->localRepo->findBySha1( $hash ); - foreach ( $this->foreignRepos as $repo ) + foreach ( $this->foreignRepos as $repo ) { $result = array_merge( $result, $repo->findBySha1( $hash ) ); + } return $result; } @@ -247,6 +289,8 @@ class RepoGroup { /** * Get the local repository, i.e. the one corresponding to the local image * table. Files are typically uploaded to the local repository. + * + * @return LocalRepo */ function getLocalRepo() { return $this->getRepo( 'local' ); @@ -307,7 +351,7 @@ class RepoGroup { */ function splitVirtualUrl( $url ) { if ( substr( $url, 0, 9 ) != 'mwrepo://' ) { - throw new MWException( __METHOD__.': unknown protoocl' ); + throw new MWException( __METHOD__.': unknown protocol' ); } $bits = explode( '/', substr( $url, 9 ), 3 ); diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php index 990a218c..2df9a9b5 100644 --- a/includes/filerepo/UnregisteredLocalFile.php +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -19,16 +19,38 @@ * @ingroup FileRepo */ class UnregisteredLocalFile extends File { - var $title, $path, $mime, $handler, $dims; + var $title, $path, $mime, $dims; + /** + * @var MediaHandler + */ + var $handler; + + /** + * @param $path + * @param $mime + * @return UnregisteredLocalFile + */ static function newFromPath( $path, $mime ) { return new UnregisteredLocalFile( false, false, $path, $mime ); } + /** + * @param $title + * @param $repo + * @return UnregisteredLocalFile + */ static function newFromTitle( $title, $repo ) { return new UnregisteredLocalFile( $title, $repo, false, false ); } + /** + * @throws MWException + * @param $title string + * @param $repo FSRepo + * @param $path string + * @param $mime string + */ 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' ); -- cgit v1.2.2