From 370e83bb0dfd0c70de268c93bf07ad5ee0897192 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Fri, 15 Aug 2008 01:29:47 +0200 Subject: Update auf 1.13.0 --- includes/filerepo/ArchivedFile.php | 72 ++-- includes/filerepo/FSRepo.php | 44 +-- includes/filerepo/File.php | 221 ++++++++---- includes/filerepo/FileRepo.php | 174 +++++++-- includes/filerepo/FileRepoStatus.php | 151 +------- includes/filerepo/ForeignAPIFile.php | 101 ++++++ includes/filerepo/ForeignAPIRepo.php | 110 ++++++ includes/filerepo/ForeignDBFile.php | 33 +- includes/filerepo/ForeignDBRepo.php | 13 +- includes/filerepo/ForeignDBViaLBRepo.php | 39 ++ includes/filerepo/Image.php | 74 ++++ includes/filerepo/LocalFile.php | 533 +++++++++++++++++++--------- includes/filerepo/LocalRepo.php | 103 +++++- includes/filerepo/NullRepo.php | 8 +- includes/filerepo/OldLocalFile.php | 192 +++++----- includes/filerepo/README | 14 +- includes/filerepo/RepoGroup.php | 74 +++- includes/filerepo/UnregisteredLocalFile.php | 9 +- 18 files changed, 1327 insertions(+), 638 deletions(-) create mode 100644 includes/filerepo/ForeignAPIFile.php create mode 100644 includes/filerepo/ForeignAPIRepo.php create mode 100644 includes/filerepo/ForeignDBViaLBRepo.php create mode 100644 includes/filerepo/Image.php (limited to 'includes/filerepo') diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index cc70b26d..646256bb 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -1,7 +1,7 @@ $this->title->getDBkey(), $conds ), __METHOD__, array( 'ORDER BY' => 'fa_timestamp DESC' ) ); - + if ( $dbr->numRows( $res ) == 0 ) { // this revision does not exist? return; } $ret = $dbr->resultObject( $res ); $row = $ret->fetchObject(); - + // initialize fields for filestore image object $this->id = intval($row->fa_id); $this->name = $row->fa_name; @@ -120,17 +120,17 @@ class ArchivedFile return; } $this->dataLoaded = true; - + return true; } /** * Loads a file object from the filearchive table * @return ResultWrapper - */ - public static function newFromRow( $row ) { + */ + public static function newFromRow( $row ) { $file = new ArchivedFile( Title::makeTitle( NS_IMAGE, $row->fa_name ) ); - + $file->id = intval($row->fa_id); $file->name = $row->fa_name; $file->archive_name = $row->fa_archive_name; @@ -148,41 +148,41 @@ class ArchivedFile $file->user_text = $row->fa_user_text; $file->timestamp = $row->fa_timestamp; $file->deleted = $row->fa_deleted; - + return $file; } - + /** * Return the associated title object * @public */ - public function getTitle() { + public function getTitle() { return $this->title; } /** * Return the file name - */ - public function getName() { + */ + public function getName() { return $this->name; } - public function getID() { + public function getID() { $this->load(); return $this->id; } - + /** * Return the FileStore key - */ - public function getKey() { + */ + public function getKey() { $this->load(); return $this->key; } - + /** * Return the FileStore storage group - */ + */ public function getGroup() { return $file->group; } @@ -192,7 +192,7 @@ class ArchivedFile */ public function getWidth() { $this->load(); - return $this->width; + return $this->width; } /** @@ -200,9 +200,9 @@ class ArchivedFile */ public function getHeight() { $this->load(); - return $this->height; + return $this->height; } - + /** * Get handler-specific metadata */ @@ -219,7 +219,7 @@ class ArchivedFile $this->load(); return $this->size; } - + /** * Return the bits of the image file, in bytes * @public @@ -228,7 +228,7 @@ class ArchivedFile $this->load(); return $this->bits; } - + /** * Returns the mime type of the file. */ @@ -236,7 +236,7 @@ class ArchivedFile $this->load(); return $this->mime; } - + /** * Return the type of the media in the file. * Use the value returned by this function with the MEDIATYPE_xxx constants. @@ -265,7 +265,7 @@ class ArchivedFile return $this->user; } } - + /** * Return the user name of the uploader. */ @@ -277,7 +277,7 @@ class ArchivedFile return $this->user_text; } } - + /** * Return upload description. */ @@ -289,7 +289,7 @@ class ArchivedFile return $this->description; } } - + /** * Return the user ID of the uploader. */ @@ -297,7 +297,7 @@ class ArchivedFile $this->load(); return $this->user; } - + /** * Return the user name of the uploader. */ @@ -305,7 +305,7 @@ class ArchivedFile $this->load(); return $this->user_text; } - + /** * Return upload description. */ @@ -322,18 +322,18 @@ class ArchivedFile public function isDeleted( $field ) { 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 int $field + * @param int $field * @return bool */ public function userCan( $field ) { if( ($this->deleted & $field) == $field ) { global $wgUser; $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED - ? 'hiderevision' + ? 'suppressrevision' : 'deleterevision'; wfDebug( "Checking for $permission due to $field match on $this->deleted\n" ); return $wgUser->isAllowed( $permission ); diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 86887d09..08ec1514 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -3,8 +3,8 @@ /** * A repository for files accessible via the local filesystem. Does not support * database access or registration. + * @ingroup FileRepo */ - class FSRepo extends FileRepo { var $directory, $deletedDir, $url, $hashLevels, $deletedHashLevels; var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' ); @@ -20,7 +20,7 @@ class FSRepo extends FileRepo { // Optional settings $this->hashLevels = isset( $info['hashLevels'] ) ? $info['hashLevels'] : 2; - $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? + $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ? $info['deletedHashLevels'] : $this->hashLevels; $this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false; } @@ -80,7 +80,7 @@ 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 + * The suffix, if supplied, is considered to be unencoded, and will be * URL-encoded before being returned. */ function getVirtualUrl( $suffix = false ) { @@ -121,10 +121,13 @@ class FSRepo extends FileRepo { * @param integer $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 + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the * same contents as the source */ function storeBatch( $triplets, $flags = 0 ) { + 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 ); } @@ -146,13 +149,13 @@ class FSRepo extends FileRepo { if ( !wfMkdirParents( $dstDir ) ) { return $this->newFatal( 'directorycreateerror', $dstDir ); } - // In the deleted zone, seed new directories with a blank + // In the deleted zone, seed new directories with a blank // index.html, to prevent crawling if ( $dstZone == 'deleted' ) { file_put_contents( "$dstDir/index.html", '' ); } } - + if ( self::isVirtualUrl( $srcPath ) ) { $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath ); } @@ -213,7 +216,7 @@ class FSRepo extends FileRepo { /** * Pick a random name in the temp zone and store a file to it. - * @param string $originalName The base name of the file as specified + * @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. @@ -255,6 +258,9 @@ class FSRepo extends FileRepo { */ function publishBatch( $triplets, $flags = 0 ) { // Perform initial checks + 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 ); } @@ -273,7 +279,7 @@ class FSRepo extends FileRepo { } $dstPath = "{$this->directory}/$dstRel"; $archivePath = "{$this->directory}/$archiveRel"; - + $dstDir = dirname( $dstPath ); $archiveDir = dirname( $archivePath ); // Abort immediately on directory creation errors since they're likely to be repetitive @@ -292,7 +298,7 @@ class FSRepo extends FileRepo { if ( !$status->ok ) { return $status; } - + foreach ( $triplets as $i => $triplet ) { list( $srcPath, $dstRel, $archiveRel ) = $triplet; $dstPath = "{$this->directory}/$dstRel"; @@ -302,8 +308,8 @@ class FSRepo extends FileRepo { if( is_file( $dstPath ) ) { // Check if the archive file exists // This is a sanity check to avoid data loss. In UNIX, the rename primitive - // unlinks the destination file if it exists. DB-based synchronisation in - // publishBatch's caller should prevent races. In Windows there's no + // unlinks the destination file if it exists. DB-based synchronisation in + // publishBatch's caller should prevent races. In Windows there's no // problem because the rename primitive fails if the destination exists. if ( is_file( $archivePath ) ) { $success = false; @@ -354,12 +360,12 @@ class FSRepo extends FileRepo { /** * Move a group of files to the deletion archive. - * If no valid deletion archive is configured, this may either delete the + * If no valid deletion archive is configured, this may either delete the * file or throw an exception, depending on the preference of the repository. * - * @param array $sourceDestPairs Array of source/destination pairs. Each 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 + * public root in the first element, and the archive file path relative * to the deleted zone root in the second element. * @return FileRepoStatus */ @@ -403,7 +409,7 @@ class FSRepo extends FileRepo { /** * Move the files - * We're now committed to returning an OK result, which will lead to + * We're now committed to returning an OK result, which will lead to * the files being moved in the DB also. */ foreach ( $sourceDestPairs as $pair ) { @@ -433,7 +439,7 @@ class FSRepo extends FileRepo { } return $status; } - + /** * Get a relative path including trailing slash, e.g. f/fa/ * If the repo is not hashed, returns an empty string @@ -443,7 +449,7 @@ class FSRepo extends FileRepo { } /** - * Get a relative path for a deletion archive key, + * Get a relative path for a deletion archive key, * e.g. s/z/a/ for sza251lrxrc1jad41h5mgilp8nysje52.jpg */ function getDeletedHashPath( $key ) { @@ -453,7 +459,7 @@ class FSRepo extends FileRepo { } return $path; } - + /** * Call a callback function for every file in the repository. * Uses the filesystem even in child classes. @@ -526,5 +532,3 @@ class FSRepo extends FileRepo { } } - - diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index 5172ad0f..64b48e0a 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -1,15 +1,15 @@ getLocalRepo()->newFile($title); @@ -17,7 +17,7 @@ * The convenience functions wfLocalFile() and wfFindFile() should be sufficient * in most cases. * - * @addtogroup FileRepo + * @ingroup FileRepo */ abstract class File { const DELETED_FILE = 1; @@ -28,17 +28,17 @@ abstract class File { const DELETE_SOURCE = 1; - /** - * Some member variables can be lazy-initialised using __get(). The + /** + * Some member variables can be lazy-initialised using __get(). The * initialisation function for these variables is always a function named - * like getVar(), where Var is the variable name with upper-case first + * like getVar(), where Var is the variable name with upper-case first * letter. * * The following variables are initialised in this way in this base class: - * name, extension, handler, path, canRender, isSafeFile, + * name, extension, handler, path, canRender, isSafeFile, * transformScript, hashPath, pageCount, url * - * Code within this class should generally use the accessor function + * Code within this class should generally use the accessor function * directly, since __get() isn't re-entrant and therefore causes bugs that * depend on initialisation order. */ @@ -46,7 +46,7 @@ abstract class File { /** * The following member variables are not lazy-initialised */ - var $repo, $title, $lastError, $redirected; + var $repo, $title, $lastError, $redirected, $redirectedTitle; /** * Call this constructor from child classes @@ -79,7 +79,8 @@ abstract class File { 'htm' => 'html', 'jpeg' => 'jpg', 'mpeg' => 'mpg', - 'tiff' => 'tif' ); + 'tiff' => 'tif', + 'ogv' => 'ogg' ); if( isset( $squish[$lower] ) ) { return $squish[$lower]; } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) { @@ -89,6 +90,21 @@ abstract class File { } } + /** + * Checks if file extensions are compatible + * + * @param $old File Old file + * @param $new string New name + */ + static function checkExtensionCompatibility( File $old, $new ) { + $oldMime = $old->getMimeType(); + $n = strrpos( $new, '.' ); + $newExt = self::normalizeExtension( + $n ? substr( $new, $n + 1 ) : '' ); + $mimeMagic = MimeMagic::singleton(); + return $mimeMagic->isMatchingExtension( $newExt, $oldMime ); + } + /** * Upgrade the database row if there is one * Called by ImagePage @@ -110,7 +126,7 @@ abstract class File { return array( $mime, 'unknown' ); } } - + /** * Return the name of this file */ @@ -118,7 +134,7 @@ abstract class File { if ( !isset( $this->name ) ) { $this->name = $this->repo->getNameFromTitle( $this->title ); } - return $this->name; + return $this->name; } /** @@ -127,7 +143,7 @@ abstract class File { function getExtension() { if ( !isset( $this->extension ) ) { $n = strrpos( $this->getName(), '.' ); - $this->extension = self::normalizeExtension( + $this->extension = self::normalizeExtension( $n ? substr( $this->getName(), $n + 1 ) : '' ); } return $this->extension; @@ -137,17 +153,26 @@ abstract class File { * Return the associated title object */ public function getTitle() { return $this->title; } + + /** + * Return the title used to find this file + */ + public function getOriginalTitle() { + if ( $this->redirected ) + return $this->getRedirectedTitle(); + return $this->title; + } /** * Return the URL of the file */ - public function getUrl() { + public function getUrl() { if ( !isset( $this->url ) ) { $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel(); } - return $this->url; + return $this->url; } - + /** * Return a fully-qualified URL to the file. * Upload URL paths _may or may not_ be fully qualified, so @@ -197,7 +222,7 @@ abstract class File { } /** - * Return the width of the image. Returns false if the width is unknown + * Return the width of the image. Returns false if the width is unknown * or undefined. * * STUB @@ -206,7 +231,7 @@ abstract class File { public function getWidth( $page = 1 ) { return false; } /** - * Return the height of the image. Returns false if the height is unknown + * Return the height of the image. Returns false if the height is unknown * or undefined * * STUB @@ -264,8 +289,8 @@ abstract class File { function getMediaType() { return MEDIATYPE_UNKNOWN; } /** - * Checks if the output of transform() for this file is likely - * to be valid. If this is false, various user elements will + * Checks if the output of transform() for this file is likely + * to be valid. If this is false, various user elements will * display a placeholder instead. * * Currently, this checks if the file is an image format @@ -325,7 +350,7 @@ abstract class File { } return $this->isSafeFile; } - + /** Accessor for __get() */ protected function getIsSafeFile() { return $this->isSafeFile(); @@ -371,13 +396,24 @@ abstract class File { * Returns true if file exists in the repository. * * Overridden by LocalFile to avoid unnecessary stat calls. - * + * * @return boolean Whether file exists in the repository. */ public function exists() { return $this->getPath() && file_exists( $this->path ); } + /** + * 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. + * @public + */ + function isVisible() { + return $this->exists(); + } + function getTransformScript() { if ( !isset( $this->transformScript ) ) { $this->transformScript = false; @@ -482,7 +518,7 @@ abstract class File { /** * Transform a media file * - * @param array $params An associative array of handler-specific parameters. Typical + * @param array $params An associative array of handler-specific parameters. Typical * keys are width, height and page. * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering * @return MediaTransformOutput @@ -509,10 +545,10 @@ abstract class File { $normalisedParams = $params; $this->handler->normaliseParams( $this, $normalisedParams ); - $thumbName = $this->thumbName( $normalisedParams ); + $thumbName = $this->thumbName( $normalisedParams ); $thumbPath = $this->getThumbPath( $thumbName ); $thumbUrl = $this->getThumbUrl( $thumbName ); - + if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); break; @@ -536,8 +572,11 @@ abstract class File { } } - if ( $wgUseSquid ) { - wfPurgeSquidServers( array( $thumbUrl ) ); + // 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->isError() || $thumb->getUrl() != $this->getURL()) ) { + SquidUpdate::purge( array( $thumbUrl ) ); } } while (false); @@ -545,7 +584,7 @@ abstract class File { return $thumb; } - /** + /** * Hook into transform() to allow migration of thumbnail files * STUB * Overridden by LocalFile @@ -555,7 +594,7 @@ abstract class File { /** * Get a MediaHandler instance for this file */ - function getHandler() { + function getHandler() { if ( !isset( $this->handler ) ) { $this->handler = MediaHandler::getHandler( $this->getMimeType() ); } @@ -614,7 +653,7 @@ abstract class File { $title->purgeSquid(); } } - + /** * Purge metadata and all affected pages when the file is created, * deleted, or majorly updated. @@ -641,12 +680,12 @@ abstract class File { * @param $end timestamp Only revisions newer than $end will be returned */ function getHistory($limit = null, $start = null, $end = null) { - return false; + return array(); } /** - * Return the history of this file, line by line. Starts with current version, - * then old versions. Should return an object similar to an image/oldimage + * Return the history of this file, line by line. Starts with current version, + * then old versions. Should return an object similar to an image/oldimage * database row. * * STUB @@ -712,7 +751,7 @@ abstract class File { /** Get the path of the archive directory, or a particular file if $suffix is specified */ function getArchivePath( $suffix = false ) { - return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel(); + return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix ); } /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */ @@ -767,7 +806,7 @@ abstract class File { $path .= '/' . rawurlencode( $suffix ); } return $path; - } + } /** * @return bool @@ -785,25 +824,25 @@ abstract class File { * STUB * Overridden by LocalFile */ - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { - $this->readOnlyError(); + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) { + $this->readOnlyError(); } /** - * Move or copy a file to its public location. If a file exists at the - * destination, move it to an archive. Returns the archive name on success - * or an empty string if it was a new file, and a wikitext-formatted - * WikiError object on failure. + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. * * The archive name should be passed through to recordUpload for database * registration. * * @param string $sourcePath Local filesystem path to the source image * @param integer $flags A bitwise combination of: - * File::DELETE_SOURCE Delete the source file, i.e. move + * File::DELETE_SOURCE Delete the source file, i.e. move * rather than copy - * @return The archive name on success or an empty string if it was a new - * file, and a wikitext-formatted WikiError object on failure. + * @return The archive name on success or an empty string if it was a new + * file, and a wikitext-formatted WikiError object on failure. * * STUB * Overridden by LocalFile @@ -829,18 +868,19 @@ abstract class File { } else { $db = wfGetDB( DB_SLAVE ); } - $linkCache =& LinkCache::singleton(); + $linkCache = LinkCache::singleton(); list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' ); $encName = $db->addQuotes( $this->getName() ); - $sql = "SELECT page_namespace,page_title,page_id FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; + $sql = "SELECT page_namespace,page_title,page_id,page_len,page_is_redirect, + FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options"; $res = $db->query( $sql, __METHOD__ ); $retVal = array(); if ( $db->numRows( $res ) ) { while ( $row = $db->fetchObject( $res ) ) { - if ( $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title ) ) { - $linkCache->addGoodLinkObj( $row->page_id, $titleObj ); + if ( $titleObj = Title::newFromRow( $row ) ) { + $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect ); $retVal[] = $titleObj; } } @@ -862,8 +902,8 @@ abstract class File { * * @return bool */ - function isLocal() { - return $this->getRepoName() == 'local'; + function isLocal() { + return $this->getRepoName() == 'local'; } /** @@ -871,8 +911,14 @@ abstract class File { * * @return string */ - function getRepoName() { - return $this->repo ? $this->repo->getName() : 'unknown'; + function getRepoName() { + return $this->repo ? $this->repo->getName() : 'unknown'; + } + /* + * Returns the repository + */ + function getRepo() { + return $this->repo; } /** @@ -901,6 +947,22 @@ abstract class File { return $title && $title->isDeleted() > 0; } + /** + * Move file to the new title + * + * Move current, old version and all thumbnails + * to the new filename. Old file is deleted. + * + * Cache purging is done; checks for validity + * and logging are caller's responsibility + * + * @param $target Title New file name + * @return FileRepoStatus object. + */ + function move( $target ) { + $this->readOnlyError(); + } + /** * Delete all versions of the file. * @@ -910,11 +972,12 @@ abstract class File { * Cache purging is done; logging is caller's responsibility. * * @param $reason + * @param $suppress, hide content from sysops? * @return true on success, false on some kind of failure * STUB * Overridden by LocalFile */ - function delete( $reason ) { + function delete( $reason, $suppress = false ) { $this->readOnlyError(); } @@ -926,12 +989,13 @@ abstract class File { * * @param $versions 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, * or false on failure * STUB * Overridden by LocalFile */ - function restore( $versions=array(), $Unsuppress=false ) { + function restore( $versions=array(), $unsuppress=false ) { $this->readOnlyError(); } @@ -971,9 +1035,9 @@ abstract class File { return round( $srcHeight * $dstWidth / $srcWidth ); } } - + /** - * Get an image size array like that returned by getimagesize(), or false if it + * Get an image size array like that returned by getimagesize(), or false if it * can't be determined. * * @param string $fileName The filename @@ -998,13 +1062,26 @@ abstract class File { * Get the HTML text of the description page, if available */ function getDescriptionText() { + global $wgMemc; if ( !$this->repo->fetchDescription ) { return false; } $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() ); if ( $renderUrl ) { + if ( $this->repo->descriptionCacheExpiry > 0 ) { + wfDebug("Attempting to get the description from cache..."); + $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($renderUrl) ); + $obj = $wgMemc->get($key); + if ($obj) { + wfDebug("success!\n"); + return $obj; + } + wfDebug("miss\n"); + } wfDebug( "Fetching shared description from $renderUrl\n" ); - return Http::get( $renderUrl ); + $res = Http::get( $renderUrl ); + if ( $res && $this->repo->descriptionCacheExpiry > 0 ) $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry ); + return $res; } else { return false; } @@ -1020,14 +1097,14 @@ abstract class File { /** * Get the 14-character timestamp of the file upload, or false if - * it doesn't exist + * it doesn't exist */ function getTimestamp() { $path = $this->getPath(); if ( !file_exists( $path ) ) { return false; } - return wfTimestamp( filemtime( $path ) ); + return wfTimestamp( TS_MW, filemtime( $path ) ); } /** @@ -1036,12 +1113,12 @@ abstract class File { function getSha1() { return self::sha1Base36( $this->getPath() ); } - + /** * Determine if the current user is allowed to view a particular * field of this file, if it's marked as deleted. * STUB - * @param int $field + * @param int $field * @return bool */ function userCan( $field ) { @@ -1052,13 +1129,13 @@ abstract class File { * Get an associative array containing information about a file in the local filesystem. * * @param string $path Absolute local filesystem path - * @param mixed $ext The file extension, or true to extract it from the filename. + * @param mixed $ext The file extension, or true to extract it from the filename. * Set it to false to ignore the extension. */ static function getPropsFromPath( $path, $ext = true ) { wfProfileIn( __METHOD__ ); wfDebug( __METHOD__.": Getting file info for $path\n" ); - $info = array( + $info = array( 'fileExists' => file_exists( $path ) && !is_dir( $path ) ); $gis = false; @@ -1111,8 +1188,8 @@ abstract class File { } /** - * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case - * encoding, zero padded to 31 digits. + * 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. @@ -1160,6 +1237,14 @@ abstract class File { function getRedirected() { return $this->redirected; } + + function getRedirectedTitle() { + if ( $this->redirected ) { + if ( !$this->redirectTitle ) + $this->redirectTitle = Title::makeTitle( NS_IMAGE, $this->redirected ); + return $this->redirectTitle; + } + } function redirectedFrom( $from ) { $this->redirected = $from; @@ -1172,5 +1257,3 @@ define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE ); define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT ); define( 'MW_IMG_DELETED_USER', File::DELETED_USER ); define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED ); - - diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index ee7691a6..edfc2a99 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -3,9 +3,12 @@ /** * Base class for file repositories * Do not instantiate, use a derived class. + * @ingroup FileRepo */ abstract class FileRepo { const DELETE_SOURCE = 1; + const FIND_PRIVATE = 1; + const FIND_IGNORE_REDIRECT = 2; const OVERWRITE = 2; const OVERWRITE_SAME = 4; @@ -13,20 +16,21 @@ abstract class FileRepo { var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital; var $pathDisclosureProtection = 'paranoid'; - /** + /** * Factory functions for creating new files * Override these in the base class */ var $fileFactory = false, $oldFileFactory = false; + var $fileFactoryKey = false, $oldFileFactoryKey = false; function __construct( $info ) { // Required settings $this->name = $info['name']; - + // Optional settings $this->initialCapital = true; // by default - foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', - 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection' ) as $var ) + foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', + 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', 'descriptionCacheExpiry' ) as $var ) { if ( isset( $info[$var] ) ) { $this->$var = $info[$var]; @@ -45,10 +49,10 @@ abstract class FileRepo { /** * Create a new File object from the local repository * @param mixed $title Title object or string - * @param mixed $time Time at which the image is supposed to have existed. - * If this is specified, the returned object will be an + * @param mixed $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 + * a current file. Repositories not supporting version * control should return false if this parameter is set. */ function newFile( $title, $time = false ) { @@ -70,13 +74,20 @@ abstract class FileRepo { } /** - * Find an instance of the named file that existed at the specified time - * Returns false if the file did not exist. Repositories not supporting + * Find an instance of the named file 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 mixed $title Title object or string * @param mixed $time 14-character timestamp, or false for the current version */ - function findFile( $title, $time = false ) { + function findFile( $title, $time = false, $flags = 0 ) { + if ( !($title instanceof Title) ) { + $title = Title::makeTitleSafe( NS_IMAGE, $title ); + if ( !is_object( $title ) ) { + return false; + } + } # First try the current version of the file to see if it precedes the timestamp $img = $this->newFile( $title ); if ( !$img ) { @@ -86,23 +97,100 @@ abstract class FileRepo { return $img; } # Now try an old version of the file - $img = $this->newFile( $title, $time ); - if ( $img->exists() ) { - return $img; + if ( $time !== false ) { + $img = $this->newFile( $title, $time ); + if ( $img->exists() ) { + if ( !$img->isDeleted(File::DELETED_FILE) ) { + return $img; + } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) { + return $img; + } + } } - + # Now try redirects - $redir = $this->checkRedirect( $title ); + if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) { + return false; + } + $redir = $this->checkRedirect( $title ); if( $redir && $redir->getNamespace() == NS_IMAGE) { $img = $this->newFile( $redir ); if( !$img ) { return false; } if( $img->exists() ) { - $img->redirectedFrom( $title->getText() ); + $img->redirectedFrom( $title->getDBkey() ); return $img; } } + return false; + } + + /* + * Find many files at once. + * @param array $titles, an array of titles + * @param int $flags + */ + function findFiles( $titles, $flags ) { + $result = array(); + foreach ( $titles as $index => $title ) { + $file = $this->findFile( $title, $flags ); + if ( $file ) + $result[$file->getTitle()->getDBkey()] = $file; + } + return $result; + } + + /** + * Create a new File object from the local repository + * @param mixed $sha1 SHA-1 key + * @param mixed $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. + */ + function newFileFromKey( $sha1, $time = false ) { + if ( $time ) { + if ( $this->oldFileFactoryKey ) { + return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); + } else { + return false; + } + } else { + return call_user_func( $this->fileFactoryKey, $sha1, $this ); + } + } + + /** + * 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 string $sha1 string + * @param mixed $time 14-character timestamp, or false for the current version + */ + function findFileFromKey( $sha1, $time = false, $flags = 0 ) { + # First try the current version of the file to see if it precedes the timestamp + $img = $this->newFileFromKey( $sha1 ); + if ( !$img ) { + return false; + } + if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) { + return $img; + } + # Now try an old version of the file + if ( $time !== false ) { + $img = $this->newFileFromKey( $sha1, $time ); + if ( $img->exists() ) { + if ( !$img->isDeleted(File::DELETED_FILE) ) { + return $img; + } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) { + return $img; + } + } + } + return false; } /** @@ -163,11 +251,11 @@ abstract class FileRepo { function getDescBaseUrl() { if ( is_null( $this->descBaseUrl ) ) { if ( !is_null( $this->articleUrl ) ) { - $this->descBaseUrl = str_replace( '$1', - wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); + $this->descBaseUrl = str_replace( '$1', + wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':', $this->articleUrl ); } elseif ( !is_null( $this->scriptDirUrl ) ) { - $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . - wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) ) . ':'; + $this->descBaseUrl = $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) ) . ':'; } else { $this->descBaseUrl = false; } @@ -177,8 +265,8 @@ abstract class FileRepo { /** * Get the URL of an image description page. May return false if it is - * unknown or not applicable. In general this should only be called by the - * File class, since it may return invalid results for certain kinds of + * unknown or not applicable. In general this should only be called by the + * File class, since it may return invalid results for certain kinds of * repositories. Use File::getDescriptionUrl() in user code. * * In particular, it uses the article paths as specified to the repository @@ -194,15 +282,15 @@ abstract class FileRepo { } /** - * Get the URL of the content-only fragment of the description page. For - * MediaWiki this means action=render. This should only be called by the - * repository's file class, since it may return invalid results. User code + * Get the URL of the content-only fragment of the description page. For + * MediaWiki this means action=render. This should only be called by the + * repository's file class, since it may return invalid results. User code * should use File::getDescriptionText(). */ function getDescriptionRenderUrl( $name ) { if ( isset( $this->scriptDirUrl ) ) { - return $this->scriptDirUrl . '/index.php?title=' . - wfUrlencode( Namespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . + return $this->scriptDirUrl . '/index.php?title=' . + wfUrlencode( MWNamespace::getCanonicalName( NS_IMAGE ) . ':' . $name ) . '&action=render'; } else { $descBase = $this->getDescBaseUrl(); @@ -223,7 +311,7 @@ abstract class FileRepo { * @param integer $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 + * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the * same contents as the source * @return FileRepoStatus */ @@ -247,7 +335,7 @@ abstract class FileRepo { * Pick a random name in the temp zone and store a file to it. * Returns a FileRepoStatus object with the URL in the value. * - * @param string $originalName The base name of the file as specified + * @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. */ @@ -268,7 +356,7 @@ abstract class FileRepo { * virtual URL, into this repository at the specified destination location. * * Returns a FileRepoStatus object. On success, the value contains "new" or - * "archived", to indicate whether the file was new with that name. + * "archived", to indicate whether the file was new with that name. * * @param string $srcPath The source path or URL * @param string $dstRel The destination relative path @@ -301,16 +389,16 @@ abstract class FileRepo { /** * Move a group of files to the deletion archive. * - * If no valid deletion archive is configured, this may either delete the + * If no valid deletion archive is configured, this may either delete the * file or throw an exception, depending on the preference of the repository. * * The overwrite policy is determined by the repository -- currently FSRepo - * assumes a naming scheme in the deleted zone based on content hash, as + * 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 Array of source/destination pairs. Each 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 + * public root in the first element, and the archive file path relative * to the deleted zone root in the second element. * @return FileRepoStatus */ @@ -318,10 +406,10 @@ abstract class FileRepo { /** * Move a file to the deletion archive. - * If no valid deletion archive exists, this may either delete the file + * If no valid deletion archive exists, this may either delete the file * or throw an exception, depending on the preference of the repository * @param mixed $srcRel Relative path for the file to be deleted - * @param mixed $archiveRel Relative path for the archive location. + * @param mixed $archiveRel Relative path for the archive location. * Relative to a private archive directory. * @return WikiError object (wikitext-formatted), or true for success */ @@ -423,5 +511,17 @@ abstract class FileRepo { function checkRedirect( $title ) { return false; } -} + /** + * Invalidates image redirect cache related to that image + * STUB + * + * @param Title $title Title of image + */ + function invalidateImageRedirect( $title ) { + } + + function findBySha1( $hash ) { + return array(); + } +} diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 5dd1dbda..63460fa8 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -1,23 +1,14 @@ value = $value; return $result; } - + function __construct( $repo = false ) { if ( $repo ) { $this->cleanCallback = $repo->getErrorCleanupFunction(); } } - - function setResult( $ok, $value = null ) { - $this->ok = $ok; - $this->value = $value; - } - - function isGood() { - return $this->ok && !$this->errors; - } - - function isOK() { - return $this->ok; - } - - function warning( $message /*, parameters... */ ) { - $params = array_slice( func_get_args(), 1 ); - $this->errors[] = array( - 'type' => 'warning', - 'message' => $message, - 'params' => $params ); - } - - /** - * Add an error, do not set fatal flag - * This can be used for non-fatal errors - */ - function error( $message /*, parameters... */ ) { - $params = array_slice( func_get_args(), 1 ); - $this->errors[] = array( - 'type' => 'error', - 'message' => $message, - 'params' => $params ); - } - - /** - * Add an error and set OK to false, indicating that the operation as a whole was fatal - */ - function fatal( $message /*, parameters... */ ) { - $params = array_slice( func_get_args(), 1 ); - $this->errors[] = array( - 'type' => 'error', - 'message' => $message, - 'params' => $params ); - $this->ok = false; - } - - protected function cleanParams( $params ) { - if ( !$this->cleanCallback ) { - return $params; - } - $cleanParams = array(); - foreach ( $params as $i => $param ) { - $cleanParams[$i] = call_user_func( $this->cleanCallback, $param ); - } - return $cleanParams; - } - - protected function getItemXML( $item ) { - $params = $this->cleanParams( $item['params'] ); - $xml = "<{$item['type']}>\n" . - Xml::element( 'message', null, $item['message'] ) . "\n" . - Xml::element( 'text', null, wfMsgReal( $item['message'], $params ) ) ."\n"; - foreach ( $params as $param ) { - $xml .= Xml::element( 'param', null, $param ); - } - $xml .= "type}>\n"; - return $xml; - } - - /** - * Get the error list as XML - */ - function getXML() { - $xml = "\n"; - foreach ( $this->errors as $error ) { - $xml .= $this->getItemXML( $error ); - } - $xml .= "\n"; - return $xml; - } - - /** - * Get the error list as a wikitext formatted list - * @param string $shortContext A short enclosing context message name, to be used - * when there is a single error - * @param string $longContext A long enclosing context message name, for a list - */ - function getWikiText( $shortContext = false, $longContext = false ) { - if ( count( $this->errors ) == 0 ) { - if ( $this->ok ) { - $this->fatal( 'internalerror_info', - __METHOD__." called for a good result, this is incorrect\n" ); - } else { - $this->fatal( 'internalerror_info', - __METHOD__.": Invalid result object: no error text but not OK\n" ); - } - } - if ( count( $this->errors ) == 1 ) { - $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $this->errors[0]['params'] ) ); - $s = wfMsgReal( $this->errors[0]['message'], $params, true, false, false ); - if ( $shortContext ) { - $s = wfMsgNoTrans( $shortContext, $s ); - } elseif ( $longContext ) { - $s = wfMsgNoTrans( $longContext, "* $s\n" ); - } - } else { - $s = ''; - foreach ( $this->errors as $error ) { - $params = array_map( 'wfEscapeWikiText', $this->cleanParams( $error['params'] ) ); - $s .= '* ' . wfMsgReal( $error['message'], $params, true, false, false ) . "\n"; - } - if ( $longContext ) { - $s = wfMsgNoTrans( $longContext, $s ); - } elseif ( $shortContext ) { - $s = wfMsgNoTrans( $shortContext, "\n* $s\n" ); - } - } - return $s; - } - - /** - * Merge another status object into this one - */ - function merge( $other, $overwriteValue = false ) { - $this->errors = array_merge( $this->errors, $other->errors ); - $this->ok = $this->ok && $other->ok; - if ( $overwriteValue ) { - $this->value = $other->value; - } - $this->successCount += $other->successCount; - $this->failCount += $other->failCount; - } } diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php new file mode 100644 index 00000000..aaf92204 --- /dev/null +++ b/includes/filerepo/ForeignAPIFile.php @@ -0,0 +1,101 @@ +mInfo = $info; + } + + static function newFromTitle( $title, $repo ) { + $info = $repo->getImageInfo( $title ); + if( $info ) { + return new ForeignAPIFile( $title, $repo, $info ); + } else { + return null; + } + } + + // Dummy functions... + public function exists() { + return true; + } + + public function getPath() { + return false; + } + + function transform( $params, $flags = 0 ) { + $thumbUrl = $this->repo->getThumbUrl( + $this->getName(), + isset( $params['width'] ) ? $params['width'] : -1, + isset( $params['height'] ) ? $params['height'] : -1 ); + if( $thumbUrl ) { + wfDebug( __METHOD__ . " got remote thumb $thumbUrl\n" ); + return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );; + } + return false; + } + + // Info we can get from API... + public function getWidth( $page = 1 ) { + return intval( @$this->mInfo['width'] ); + } + + public function getHeight( $page = 1 ) { + return intval( @$this->mInfo['height'] ); + } + + public function getMetadata() { + return serialize( (array)@$this->mInfo['metadata'] ); + } + + public function getSize() { + return intval( @$this->mInfo['size'] ); + } + + public function getUrl() { + return strval( @$this->mInfo['url'] ); + } + + public function getUser( $method='text' ) { + return strval( @$this->mInfo['user'] ); + } + + public function getDescription() { + return strval( @$this->mInfo['comment'] ); + } + + function getSha1() { + return wfBaseConvert( strval( @$this->mInfo['sha1'] ), 16, 36, 31 ); + } + + function getTimestamp() { + return wfTimestamp( TS_MW, strval( @$this->mInfo['timestamp'] ) ); + } + + function getMimeType() { + if( empty( $info['mime'] ) ) { + $magic = MimeMagic::singleton(); + $info['mime'] = $magic->guessTypesForExtension( $this->getExtension() ); + } + return $info['mime']; + } + + /// @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() ); + } + + function getDescriptionUrl() { + return isset( $this->mInfo['descriptionurl'] ) + ? $this->mInfo['descriptionurl'] + : false; + } +} diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php new file mode 100644 index 00000000..0dee699f --- /dev/null +++ b/includes/filerepo/ForeignAPIRepo.php @@ -0,0 +1,110 @@ + 'ForeignAPIRepo', + * 'name' => 'shared', + * 'apibase' => 'http://en.wikipedia.org/w/api.php', + * 'fetchDescription' => true, // Optional + * 'descriptionCacheExpiry' => 3600, + * ); + * + * @ingroup FileRepo + */ +class ForeignAPIRepo extends FileRepo { + var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' ); + protected $mQueryCache = array(); + + function __construct( $info ) { + parent::__construct( $info ); + $this->mApiBase = $info['apibase']; // http://commons.wikimedia.org/w/api.php + if( !$this->scriptDirUrl ) { + // hack for description fetches + $this->scriptDirUrl = dirname( $this->mApiBase ); + } + } + + function storeBatch( $triplets, $flags = 0 ) { + return false; + } + + function storeTemp( $originalName, $srcPath ) { + return false; + } + function publishBatch( $triplets, $flags = 0 ) { + return false; + } + function deleteBatch( $sourceDestPairs ) { + return false; + } + function getFileProps( $virtualUrl ) { + return false; + } + + protected function queryImage( $query ) { + $data = $this->fetchImageQuery( $query ); + + if( isset( $data['query']['pages'] ) ) { + foreach( $data['query']['pages'] as $pageid => $info ) { + if( isset( $info['imageinfo'][0] ) ) { + return $info['imageinfo'][0]; + } + } + } + return false; + } + + protected function fetchImageQuery( $query ) { + global $wgMemc; + + $url = $this->mApiBase . + '?' . + wfArrayToCgi( + array_merge( $query, + array( + 'format' => 'json', + 'action' => 'query', + 'prop' => 'imageinfo' ) ) ); + + if( !isset( $this->mQueryCache[$url] ) ) { + $key = wfMemcKey( 'ForeignAPIRepo', $url ); + $data = $wgMemc->get( $key ); + if( !$data ) { + $data = Http::get( $url ); + $wgMemc->set( $key, $data, 3600 ); + } + + if( count( $this->mQueryCache ) > 100 ) { + // Keep the cache from growing infinitely + $this->mQueryCache = array(); + } + $this->mQueryCache[$url] = $data; + } + return json_decode( $this->mQueryCache[$url], true ); + } + + function getImageInfo( $title, $time = false ) { + return $this->queryImage( array( + 'titles' => 'Image:' . $title->getText(), + 'iiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime' ) ); + } + + function getThumbUrl( $name, $width=-1, $height=-1 ) { + $info = $this->queryImage( array( + 'titles' => 'Image:' . $name, + 'iiprop' => 'url', + 'iiurlwidth' => $width, + 'iiurlheight' => $height ) ); + if( $info ) { + return $info['thumburl']; + } else { + return false; + } + } +} diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php index 4d11640a..eed26048 100644 --- a/includes/filerepo/ForeignDBFile.php +++ b/includes/filerepo/ForeignDBFile.php @@ -1,34 +1,52 @@ img_name ); + $file = new self( $title, $repo ); + $file->loadFromRow( $row ); + return $file; + } + function getCacheKey() { if ( $this->repo->hasSharedCache ) { $hashedName = md5($this->name); - return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix, + return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix, 'file', $hashedName ); } else { return false; } } - function publish( /*...*/ ) { + function publish( $srcPath, $flags = 0 ) { $this->readOnlyError(); } - function recordUpload( /*...*/ ) { + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', + $watch = false, $timestamp = false ) { $this->readOnlyError(); } - function restore( /*...*/ ) { + function restore( $versions = array(), $unsuppress = false ) { $this->readOnlyError(); } - function delete( /*...*/ ) { + function delete( $reason, $suppress = false ) { $this->readOnlyError(); } - + function move( $target ) { + $this->readOnlyError(); + } + function getDescriptionUrl() { // Restore remote behaviour return File::getDescriptionUrl(); @@ -39,4 +57,3 @@ class ForeignDBFile extends LocalFile { return File::getDescriptionText(); } } - diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 13dcd029..e078dd25 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -2,16 +2,17 @@ /** * A foreign repository with an accessible MediaWiki database + * @ingroup FileRepo */ - class ForeignDBRepo extends LocalRepo { # Settings - var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags, + var $dbType, $dbServer, $dbUser, $dbPassword, $dbName, $dbFlags, $tablePrefix, $hasSharedCache; - + # Other stuff var $dbConn; var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' ); + var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' ); function __construct( $info ) { parent::__construct( $info ); @@ -28,8 +29,8 @@ class ForeignDBRepo extends LocalRepo { function getMasterDB() { if ( !isset( $this->dbConn ) ) { $class = 'Database' . ucfirst( $this->dbType ); - $this->dbConn = new $class( $this->dbServer, $this->dbUser, - $this->dbPassword, $this->dbName, false, $this->dbFlags, + $this->dbConn = new $class( $this->dbServer, $this->dbUser, + $this->dbPassword, $this->dbName, false, $this->dbFlags, $this->tablePrefix ); } return $this->dbConn; @@ -53,5 +54,3 @@ class ForeignDBRepo extends LocalRepo { throw new MWException( get_class($this) . ': write operations are not supported' ); } } - - diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php new file mode 100644 index 00000000..13c9f434 --- /dev/null +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -0,0 +1,39 @@ +wiki = $info['wiki']; + list( $this->dbName, $this->tablePrefix ) = wfSplitWikiID( $this->wiki ); + $this->hasSharedCache = $info['hasSharedCache']; + } + + function getMasterDB() { + return wfGetDB( DB_MASTER, array(), $this->wiki ); + } + + function getSlaveDB() { + return wfGetDB( DB_SLAVE, array(), $this->wiki ); + } + function hasSharedCache() { + return $this->hasSharedCache; + } + + function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } + function deleteBatch( $fileMap ) { + throw new MWException( get_class($this) . ': write operations are not supported' ); + } +} diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php new file mode 100644 index 00000000..665dd4bf --- /dev/null +++ b/includes/filerepo/Image.php @@ -0,0 +1,74 @@ +getLocalRepo(); + parent::__construct( $title, $repo ); + } + + /** + * Wrapper for wfFindFile(), for backwards-compatibility only + * Do not use in core code. + * @deprecated + */ + static function newFromTitle( $title, $time = false ) { + wfDeprecated( __METHOD__ ); + $img = wfFindFile( $title, $time ); + if ( !$img ) { + $img = wfLocalFile( $title ); + } + return $img; + } + + /** + * Wrapper for wfFindFile(), for backwards-compatibility only. + * Do not use in core code. + * + * @param string $name 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_IMAGE, $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 string $name Name of the image, without the leading "Image:" + * @param boolean $fromSharedDirectory 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 9b06fe2d..57c0703d 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -5,7 +5,7 @@ /** * Bump this number when serialized cache records may be incompatible. */ -define( 'MW_FILE_VERSION', 7 ); +define( 'MW_FILE_VERSION', 8 ); /** * Class to represent a local file in the wiki's own database @@ -14,7 +14,7 @@ define( 'MW_FILE_VERSION', 7 ); * to generate image thumbnails or for uploading. * * Note that only the repo object knows what its file class is called. You should - * never name a file class explictly outside of the repo class. Instead use the + * never name a file class explictly outside of the repo class. Instead use the * repo's factory functions to generate file objects, for example: * * RepoGroup::singleton()->getLocalRepo()->newFile($title); @@ -22,7 +22,7 @@ define( 'MW_FILE_VERSION', 7 ); * The convenience functions wfLocalFile() and wfFindFile() should be sufficient * in most cases. * - * @addtogroup FileRepo + * @ingroup FileRepo */ class LocalFile extends File { @@ -48,15 +48,18 @@ class LocalFile extends File $description, # Description of current revision of the file $dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx) $upgraded, # Whether the row was upgraded on load - $locked; # True if the image row is locked + $locked, # True if the image row is locked + $deleted; # Bitfield akin to rev_deleted /**#@-*/ /** * Create a LocalFile from a title * Do not call this except from inside a repo class. + * + * Note: $unused param is only here to avoid an E_STRICT */ - static function newFromTitle( $title, $repo ) { + static function newFromTitle( $title, $repo, $unused = null ) { return new self( $title, $repo ); } @@ -70,6 +73,48 @@ class LocalFile extends File $file->loadFromRow( $row ); return $file; } + + /** + * Create a LocalFile from a SHA-1 key + * Do not call this except from inside a repo class. + */ + static function newFromKey( $sha1, $repo, $timestamp = false ) { + # Polymorphic function name to distinguish foreign and local fetches + $fname = get_class( $this ) . '::' . __FUNCTION__; + + $conds = array( 'img_sha1' => $sha1 ); + if( $timestamp ) { + $conds['img_timestamp'] = $timestamp; + } + $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), $conds, $fname ); + if( $row ) { + return self::newFromRow( $row, $repo ); + } else { + return false; + } + } + + /** + * Fields in the image table + */ + static function selectFields() { + return array( + 'img_name', + 'img_size', + 'img_width', + 'img_height', + 'img_metadata', + 'img_bits', + 'img_media_type', + 'img_major_mime', + 'img_minor_mime', + 'img_description', + 'img_user', + 'img_user_text', + 'img_timestamp', + 'img_sha1', + ); + } /** * Constructor. @@ -156,7 +201,7 @@ class LocalFile extends File } function getCacheFields( $prefix = 'img_' ) { - static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', + static $fields = array( 'size', 'width', 'height', 'bits', 'media_type', 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' ); static $results = array(); if ( $prefix == '' ) { @@ -197,7 +242,7 @@ class LocalFile extends File } /** - * Decode a row from the database (either object or array) to an array + * Decode a row from the database (either object or array) to an array * with timestamps and MIME types decoded, and the field prefix removed. */ function decodeRow( $row, $prefix = 'img_' ) { @@ -235,7 +280,6 @@ class LocalFile extends File $this->$name = $value; } $this->fileExists = true; - // Check for rows from a previous schema, quietly upgrade them $this->maybeUpgradeRow(); } @@ -259,7 +303,7 @@ class LocalFile extends File if ( wfReadOnly() ) { return; } - if ( is_null($this->media_type) || + if ( is_null($this->media_type) || $this->mime == 'image/svg' ) { $this->upgradeRow(); @@ -316,10 +360,10 @@ class LocalFile extends File } /** - * Set properties in this object to be equal to those given in the + * Set properties in this object to be equal to those given in the * associative array $info. Only cacheable fields can be set. - * - * If 'mime' is given, it will be split into major_mime/minor_mime. + * + * 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. */ function setProps( $info ) { @@ -345,6 +389,7 @@ class LocalFile extends File /** getURL inherited */ /** getViewURL inherited */ /** getPath inherited */ + /** isVisible inhereted */ /** * Return the width of the image @@ -456,7 +501,7 @@ class LocalFile extends File /** createThumb inherited */ /** getThumbnail inherited */ /** transform inherited */ - + /** * Fix thumbnail files from 1.4 or before, with extreme prejudice */ @@ -493,25 +538,21 @@ class LocalFile extends File * Get all thumbnail names previously generated for this file */ function getThumbnails() { - if ( $this->isHashed() ) { - $this->load(); - $files = array(); - $dir = $this->getThumbPath(); - - if ( is_dir( $dir ) ) { - $handle = opendir( $dir ); - - if ( $handle ) { - while ( false !== ( $file = readdir($handle) ) ) { - if ( $file{0} != '.' ) { - $files[] = $file; - } + $this->load(); + $files = array(); + $dir = $this->getThumbPath(); + + if ( is_dir( $dir ) ) { + $handle = opendir( $dir ); + + if ( $handle ) { + while ( false !== ( $file = readdir($handle) ) ) { + if ( $file{0} != '.' ) { + $files[] = $file; } - closedir( $handle ); } + closedir( $handle ); } - } else { - $files = array(); } return $files; @@ -547,7 +588,7 @@ class LocalFile extends File $this->purgeThumbnails(); // Purge squid cache for this file - wfPurgeSquidServers( array( $this->getURL() ) ); + SquidUpdate::purge( array( $this->getURL() ) ); } /** @@ -571,7 +612,7 @@ class LocalFile extends File // Purge the squid if ( $wgUseSquid ) { - wfPurgeSquidServers( $urls ); + SquidUpdate::purge( $urls ); } } @@ -580,6 +621,9 @@ class LocalFile extends File function getHistory($limit = null, $start = null, $end = null) { $dbr = $this->repo->getSlaveDB(); + $tables = array('oldimage'); + $join_conds = array(); + $fields = OldLocalFile::selectFields(); $conds = $opts = array(); $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() ); if( $start !== null ) { @@ -592,14 +636,17 @@ class LocalFile extends File $opts['LIMIT'] = $limit; } $opts['ORDER BY'] = 'oi_timestamp DESC'; - $res = $dbr->select('oldimage', '*', $conds, __METHOD__, $opts); + + wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, &$conds, &$opts, &$join_conds ) ); + + $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds ); $r = array(); while( $row = $dbr->fetchObject($res) ) { $r[] = OldLocalFile::newFromRow($row, $this->repo); } return $r; } - + /** * Return the history of this file, line by line. * starts with current version, then old versions. @@ -617,10 +664,12 @@ class LocalFile extends File $dbr = $this->repo->getSlaveDB(); if ( $this->historyLine == 0 ) {// called for the first time, return line from cur - $this->historyRes = $dbr->select( 'image', + $this->historyRes = $dbr->select( 'image', array( '*', - "'' AS oi_archive_name" + "'' AS oi_archive_name", + '0 as oi_deleted', + 'img_sha1' ), array( 'img_name' => $this->title->getDBkey() ), $fname @@ -632,7 +681,7 @@ class LocalFile extends File } } else if ( $this->historyLine == 1 ) { $dbr->freeResult($this->historyRes); - $this->historyRes = $dbr->select( 'oldimage', '*', + $this->historyRes = $dbr->select( 'oldimage', '*', array( 'oi_name' => $this->title->getDBkey() ), $fname, array( 'ORDER BY' => 'oi_timestamp DESC' ) @@ -681,12 +730,12 @@ class LocalFile extends File * @param string $timestamp Timestamp for img_timestamp, or false to use the current time * * @return FileRepoStatus object. On success, the value member contains the - * archive name, or an empty string if it was a new file. + * archive name, or an empty string if it was a new file. */ function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false ) { $this->lock(); $status = $this->publish( $srcPath, $flags ); - if ( $status->ok ) { + if ( $status->ok ) { if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp ) ) { $status->fatal( 'filenotfound', $srcPath ); } @@ -699,8 +748,8 @@ 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 ) + function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', + $watch = false, $timestamp = false ) { $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source ); if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) { @@ -717,7 +766,7 @@ class LocalFile extends File /** * Record a file upload in the upload log and the image table */ - function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false ) + function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false ) { global $wgUser; @@ -727,7 +776,7 @@ class LocalFile extends File $props = $this->repo->getFileProps( $this->getVirtualUrl() ); } $props['description'] = $comment; - $props['user'] = $wgUser->getID(); + $props['user'] = $wgUser->getId(); $props['user_text'] = $wgUser->getName(); $props['timestamp'] = wfTimestamp( TS_MW ); $this->setProps( $props ); @@ -735,7 +784,7 @@ class LocalFile extends File // Delete thumbnails and refresh the metadata cache $this->purgeThumbnails(); $this->saveToCache(); - wfPurgeSquidServers( array( $this->getURL() ) ); + SquidUpdate::purge( array( $this->getURL() ) ); // Fail now if the file isn't there if ( !$this->fileExists ) { @@ -763,7 +812,7 @@ class LocalFile extends File 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, - 'img_user' => $wgUser->getID(), + 'img_user' => $wgUser->getId(), 'img_user_text' => $wgUser->getName(), 'img_metadata' => $this->metadata, 'img_sha1' => $this->sha1 @@ -774,7 +823,7 @@ class LocalFile extends File if( $dbw->affectedRows() == 0 ) { $reupload = true; - + # Collision, this is an update of a file # Insert previous contents into oldimage $dbw->insertSelect( 'oldimage', 'image', @@ -793,7 +842,7 @@ class LocalFile extends File 'oi_media_type' => 'img_media_type', 'oi_major_mime' => 'img_major_mime', 'oi_minor_mime' => 'img_minor_mime', - 'oi_sha1' => 'img_sha1', + 'oi_sha1' => 'img_sha1' ), array( 'img_name' => $this->getName() ), __METHOD__ ); @@ -809,7 +858,7 @@ class LocalFile extends File 'img_minor_mime' => $this->minor_mime, 'img_timestamp' => $timestamp, 'img_description' => $comment, - 'img_user' => $wgUser->getID(), + 'img_user' => $wgUser->getId(), 'img_user_text' => $wgUser->getName(), 'img_metadata' => $this->metadata, 'img_sha1' => $this->sha1 @@ -836,6 +885,8 @@ class LocalFile extends File # Create a null revision $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), $log->getRcComment(), false ); $nullRevision->insertOn( $dbw ); + + wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, false) ); $article->updateRevisionOn( $dbw, $nullRevision ); # Invalidate the cache for the description page @@ -857,25 +908,31 @@ class LocalFile extends File # Invalidate cache for all pages using this file $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ); $update->doUpdate(); + # Invalidate cache for all pages that redirects on this page + $redirs = $this->getTitle()->getRedirectsHere(); + foreach( $redirs as $redir ) { + $update = new HTMLCacheUpdate( $redir, 'imagelinks' ); + $update->doUpdate(); + } return true; } /** - * Move or copy a file to its public location. If a file exists at the - * destination, move it to an archive. Returns the archive name on success - * or an empty string if it was a new file, and a wikitext-formatted - * WikiError object on failure. + * Move or copy a file to its public location. If a file exists at the + * destination, move it to an archive. Returns the archive name on success + * or an empty string if it was a new file, and a wikitext-formatted + * WikiError object on failure. * * The archive name should be passed through to recordUpload for database * registration. * * @param string $sourcePath Local filesystem path to the source image * @param integer $flags A bitwise combination of: - * File::DELETE_SOURCE Delete the source file, i.e. move + * 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. + * archive name, or an empty string if it was a new file. */ function publish( $srcPath, $flags = 0 ) { $this->lock(); @@ -897,7 +954,44 @@ class LocalFile extends File /** getExifData inherited */ /** isLocal inherited */ /** wasDeleted inherited */ - + + /** + * Move file to the new title + * + * Move current, old version and all thumbnails + * to the new filename. Old file is deleted. + * + * Cache purging is done; checks for validity + * and logging are caller's responsibility + * + * @param $target Title New file name + * @return FileRepoStatus object. + */ + function move( $target ) { + wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() ); + $this->lock(); + $batch = new LocalFileMoveBatch( $this, $target ); + $batch->addCurrent(); + $batch->addOlds(); + + $status = $batch->execute(); + wfDebugLog( 'imagemove', "Finished moving {$this->name}" ); + $this->purgeEverything(); + $this->unlock(); + + if ( $status->isOk() ) { + // Now switch the object + $this->title = $target; + // Force regeneration of the name and hashpath + unset( $this->name ); + unset( $this->hashPath ); + // Purge the new image + $this->purgeEverything(); + } + + return $status; + } + /** * Delete all versions of the file. * @@ -907,11 +1001,12 @@ class LocalFile extends File * Cache purging is done; logging is caller's responsibility. * * @param $reason + * @param $suppress * @return FileRepoStatus object. */ - function delete( $reason ) { + function delete( $reason, $suppress = false ) { $this->lock(); - $batch = new LocalFileDeleteBatch( $this, $reason ); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); $batch->addCurrent(); # Get old version relative paths @@ -944,12 +1039,13 @@ class LocalFile extends File * Cache purging is done; logging is caller's responsibility. * * @param $reason + * @param $suppress * @throws MWException or FSException on database or filestore failure * @return FileRepoStatus object. */ - function deleteOld( $archiveName, $reason ) { + function deleteOld( $archiveName, $reason, $suppress=false ) { $this->lock(); - $batch = new LocalFileDeleteBatch( $this, $reason ); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); $batch->addOld( $archiveName ); $status = $batch->execute(); $this->unlock(); @@ -968,10 +1064,11 @@ class LocalFile extends File * * @param $versions set of record ids of deleted items to restore, * or empty to restore all revisions. + * @param $unuppress * @return FileRepoStatus */ function restore( $versions = array(), $unsuppress = false ) { - $batch = new LocalFileRestoreBatch( $this ); + $batch = new LocalFileRestoreBatch( $this, $unsuppress ); if ( !$versions ) { $batch->addAll(); } else { @@ -993,9 +1090,9 @@ class LocalFile extends File /** pageCount inherited */ /** scaleHeight inherited */ /** getImageSize inherited */ - + /** - * Get the URL of the file description page. + * Get the URL of the file description page. */ function getDescriptionUrl() { return $this->title->getLocalUrl(); @@ -1033,7 +1130,7 @@ class LocalFile extends File $this->sha1 = File::sha1Base36( $this->getPath() ); if ( strval( $this->sha1 ) != '' ) { $dbw = $this->repo->getMasterDB(); - $dbw->update( 'image', + $dbw->update( 'image', array( 'img_sha1' => $this->sha1 ), array( 'img_name' => $this->getName() ), __METHOD__ ); @@ -1059,7 +1156,7 @@ class LocalFile extends File } /** - * Decrement the lock reference count. If the reference count is reduced to zero, commits + * Decrement the lock reference count. If the reference count is reduced to zero, commits * the transaction and thereby releases the image lock. */ function unlock() { @@ -1084,85 +1181,18 @@ class LocalFile extends File #------------------------------------------------------------------------------ -/** - * Backwards compatibility class - */ -class Image extends LocalFile { - function __construct( $title ) { - $repo = RepoGroup::singleton()->getLocalRepo(); - parent::__construct( $title, $repo ); - } - - /** - * Wrapper for wfFindFile(), for backwards-compatibility only - * Do not use in core code. - * @deprecated - */ - static function newFromTitle( $title, $time = false ) { - $img = wfFindFile( $title, $time ); - if ( !$img ) { - $img = wfLocalFile( $title ); - } - return $img; - } - - /** - * Wrapper for wfFindFile(), for backwards-compatibility only. - * Do not use in core code. - * - * @param string $name 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 ) { - $title = Title::makeTitleSafe( NS_IMAGE, $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 string $name Name of the image, without the leading "Image:" - * @param boolean $fromSharedDirectory Should this be in $wgSharedUploadPath? - * @return string URL of $name image - * @deprecated - */ - static function imageUrl( $name, $fromSharedDirectory = false ) { - $image = null; - if( $fromSharedDirectory ) { - $image = wfFindFile( $name ); - } - if( !$image ) { - $image = wfLocalFile( $name ); - } - return $image->getUrl(); - } -} - -#------------------------------------------------------------------------------ - /** * Helper class for file deletion + * @ingroup FileRepo */ class LocalFileDeleteBatch { - var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch; + var $file, $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress; var $status; - function __construct( File $file, $reason = '' ) { + function __construct( File $file, $reason = '', $suppress = false ) { $this->file = $file; $this->reason = $reason; + $this->suppress = $suppress; $this->status = $file->repo->newGood(); } @@ -1205,7 +1235,7 @@ class LocalFileDeleteBatch { $props = $this->file->repo->getFileProps( $oldUrl ); if ( $props['fileExists'] ) { // Upgrade the oldimage row - $dbw->update( 'oldimage', + $dbw->update( 'oldimage', array( 'oi_sha1' => $props['sha1'] ), array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ), __METHOD__ ); @@ -1244,6 +1274,18 @@ class LocalFileDeleteBatch { $encExt = $dbw->addQuotes( $dotExt ); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + // Bitfields to further suppress the content + if ( $this->suppress ) { + $bitfield = 0; + // This should be 15... + $bitfield |= Revision::DELETED_TEXT; + $bitfield |= Revision::DELETED_COMMENT; + $bitfield |= Revision::DELETED_USER; + $bitfield |= Revision::DELETED_RESTRICTED; + } else { + $bitfield = 'oi_deleted'; + } + if ( $deleteCurrent ) { $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) ); $where = array( 'img_name' => $this->file->getName() ); @@ -1254,7 +1296,7 @@ class LocalFileDeleteBatch { 'fa_deleted_user' => $encUserId, 'fa_deleted_timestamp' => $encTimestamp, 'fa_deleted_reason' => $encReason, - 'fa_deleted' => 0, + 'fa_deleted' => $this->suppress ? $bitfield : 0, 'fa_name' => 'img_name', 'fa_archive_name' => 'NULL', @@ -1278,14 +1320,14 @@ class LocalFileDeleteBatch { $where = array( 'oi_name' => $this->file->getName(), 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ); - $dbw->insertSelect( 'filearchive', 'oldimage', + $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_deleted_timestamp' => $encTimestamp, 'fa_deleted_reason' => $encReason, - 'fa_deleted' => 0, + 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted', 'fa_name' => 'oi_name', 'fa_archive_name' => 'oi_archive_name', @@ -1300,7 +1342,8 @@ class LocalFileDeleteBatch { 'fa_description' => 'oi_description', 'fa_user' => 'oi_user', 'fa_user_text' => 'oi_user_text', - 'fa_timestamp' => 'oi_timestamp' + 'fa_timestamp' => 'oi_timestamp', + 'fa_deleted' => $bitfield ), $where, __METHOD__ ); } } @@ -1309,10 +1352,10 @@ class LocalFileDeleteBatch { $dbw = $this->file->repo->getMasterDB(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); if ( count( $oldRels ) ) { - $dbw->delete( 'oldimage', + $dbw->delete( 'oldimage', array( 'oi_name' => $this->file->getName(), - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' ), __METHOD__ ); } if ( $deleteCurrent ) { @@ -1328,15 +1371,30 @@ class LocalFileDeleteBatch { wfProfileIn( __METHOD__ ); $this->file->lock(); - + // Leave private files alone + $privateFiles = array(); + list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + $dbw = $this->file->repo->getMasterDB(); + if( !empty( $oldRels ) ) { + $res = $dbw->select( 'oldimage', + array( 'oi_archive_name' ), + array( 'oi_name' => $this->file->getName(), + 'oi_archive_name IN (' . $dbw->makeList( array_keys($oldRels) ) . ')', + 'oi_deleted & ' . File::DELETED_FILE => File::DELETED_FILE ), + __METHOD__ ); + while( $row = $dbw->fetchObject( $res ) ) { + $privateFiles[$row->oi_archive_name] = 1; + } + } // Prepare deletion batch $hashes = $this->getHashes(); $this->deletionBatch = array(); $ext = $this->file->getExtension(); $dotExt = $ext === '' ? '' : ".$ext"; foreach ( $this->srcRels as $name => $srcRel ) { - // Skip files that have no hash (missing source) - if ( isset( $hashes[$name] ) ) { + // Skip files that have no hash (missing source). + // Keep private files where they are. + if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) { $hash = $hashes[$name]; $key = $hash . $dotExt; $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key; @@ -1347,7 +1405,7 @@ class LocalFileDeleteBatch { // Lock the filearchive rows so that the files don't get deleted by a cleanup operation // We acquire this lock by running the inserts now, before the file operations. // - // This potentially has poor lock contention characteristics -- an alternative + // This potentially has poor lock contention characteristics -- an alternative // scheme would be to insert stub filearchive entries with no fa_name and commit // them in a separate transaction, then run the file ops, then update the fa_name fields. $this->doDBInserts(); @@ -1390,14 +1448,16 @@ class LocalFileDeleteBatch { /** * Helper class for file undeletion + * @ingroup FileRepo */ class LocalFileRestoreBatch { var $file, $cleanupBatch, $ids, $all, $unsuppress = false; - function __construct( File $file ) { + function __construct( File $file, $unsuppress = false ) { $this->file = $file; $this->cleanupBatch = $this->ids = array(); $this->ids = array(); + $this->unsuppress = $unsuppress; } /** @@ -1420,9 +1480,9 @@ class LocalFileRestoreBatch { function addAll() { $this->all = true; } - + /** - * Run the transaction, except the cleanup batch. + * Run the transaction, except the cleanup batch. * The cleanup batch should be run in a separate transaction, because it locks different * rows and there's no need to keep the image row locked while it's acquiring those locks * The caller may have its own transaction open. @@ -1438,7 +1498,7 @@ class LocalFileRestoreBatch { $exists = $this->file->lock(); $dbw = $this->file->repo->getMasterDB(); $status = $this->file->repo->newGood(); - + // Fetch all or selected archived revisions for the file, // sorted from the most recent to the oldest. $conditions = array( 'fa_name' => $this->file->getName() ); @@ -1460,12 +1520,7 @@ class LocalFileRestoreBatch { $archiveNames = array(); while( $row = $dbw->fetchObject( $result ) ) { $idsPresent[] = $row->fa_id; - if ( $this->unsuppress ) { - // Currently, fa_deleted flags fall off upon restore, lets be careful about this - } else if ( ($row->fa_deleted & Revision::DELETED_RESTRICTED) && !$wgUser->isAllowed('hiderevision') ) { - // Skip restoring file revisions that the user cannot restore - continue; - } + if ( $row->fa_name != $this->file->getName() ) { $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) ); $status->failCount++; @@ -1486,7 +1541,7 @@ class LocalFileRestoreBatch { if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) { $sha1 = substr( $sha1, 1 ); } - + 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' @@ -1503,6 +1558,11 @@ class LocalFileRestoreBatch { } if ( $first && !$exists ) { + // The live (current) version cannot be hidden! + if( !$this->unsuppress && $row->fa_deleted ) { + $this->file->unlock(); + return $status; + } // This revision will be published as the new current version $destRel = $this->file->getRel(); $insertCurrent = array( @@ -1549,13 +1609,17 @@ class LocalFileRestoreBatch { 'oi_media_type' => $props['media_type'], 'oi_major_mime' => $props['major_mime'], 'oi_minor_mime' => $props['minor_mime'], - 'oi_deleted' => $row->fa_deleted, + 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted, 'oi_sha1' => $sha1 ); } $deleteIds[] = $row->fa_id; - $storeBatch[] = array( $deletedUrl, 'public', $destRel ); - $this->cleanupBatch[] = $row->fa_storage_key; + if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) { + // private files can stay where they are + } else { + $storeBatch[] = array( $deletedUrl, 'public', $destRel ); + $this->cleanupBatch[] = $row->fa_storage_key; + } $first = false; } unset( $result ); @@ -1580,8 +1644,8 @@ class LocalFileRestoreBatch { // Run the DB updates // Because we have locked the image row, key conflicts should be rare. - // If they do occur, we can roll back the transaction at this time with - // no data loss, but leaving unregistered files scattered throughout the + // If they do occur, we can roll back the transaction at this time with + // no data loss, but leaving unregistered files scattered throughout the // public zone. // This is not ideal, which is why it's important to lock the image row. if ( $insertCurrent ) { @@ -1591,8 +1655,8 @@ class LocalFileRestoreBatch { $dbw->insert( 'oldimage', $insertBatch, __METHOD__ ); } if ( $deleteIds ) { - $dbw->delete( 'filearchive', - array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), + $dbw->delete( 'filearchive', + array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), __METHOD__ ); } @@ -1627,3 +1691,146 @@ class LocalFileRestoreBatch { return $status; } } + +#------------------------------------------------------------------------------ + +/** + * Helper class for file movement + * @ingroup FileRepo + */ +class LocalFileMoveBatch { + var $file, $cur, $olds, $oldCount, $archive, $target, $db; + + function __construct( File $file, Title $target ) { + $this->file = $file; + $this->target = $target; + $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() ); + $this->newHash = $this->file->repo->getHashPath( $this->target->getDbKey() ); + $this->oldName = $this->file->getName(); + $this->newName = $this->file->repo->getNameFromTitle( $this->target ); + $this->oldRel = $this->oldHash . $this->oldName; + $this->newRel = $this->newHash . $this->newName; + $this->db = $file->repo->getMasterDb(); + } + + /* + * Add the current image to the batch + */ + function addCurrent() { + $this->cur = array( $this->oldRel, $this->newRel ); + } + + /* + * Add the old versions of the image to the batch + */ + function addOlds() { + $archiveBase = 'archive'; + $this->olds = array(); + $this->oldCount = 0; + + $result = $this->db->select( 'oldimage', + array( 'oi_archive_name', 'oi_deleted' ), + array( 'oi_name' => $this->oldName ), + __METHOD__ + ); + while( $row = $this->db->fetchObject( $result ) ) { + $oldName = $row->oi_archive_name; + $bits = explode( '!', $oldName, 2 ); + if( count( $bits ) != 2 ) { + wfDebug( 'Invalid old file name: ' . $oldName ); + continue; + } + list( $timestamp, $filename ) = $bits; + if( $this->oldName != $filename ) { + wfDebug( 'Invalid old file name:' . $oldName ); + continue; + } + $this->oldCount++; + // Do we want to add those to oldCount? + if( $row->oi_deleted & File::DELETED_FILE ) { + continue; + } + $this->olds[] = array( + "{$archiveBase}/{$this->oldHash}{$oldname}", + "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}" + ); + } + $this->db->freeResult( $result ); + } + + /* + * Perform the move. + */ + function execute() { + $repo = $this->file->repo; + $status = $repo->newGood(); + $triplets = $this->getMoveTriplets(); + + $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() ) { + wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() ); + $this->db->rollback(); + } + $status->merge( $statusDb ); + $status->merge( $statusMove ); + return $status; + } + + /* + * Do the database updates and return a new WikiError indicating how many + * rows where updated. + */ + function doDBUpdates() { + $repo = $this->file->repo; + $status = $repo->newGood(); + $dbw = $this->db; + + // Update current image + $dbw->update( + 'image', + array( 'img_name' => $this->newName ), + array( 'img_name' => $this->oldName ), + __METHOD__ + ); + if( $dbw->affectedRows() ) { + $status->successCount++; + } else { + $status->failCount++; + } + + // Update old images + $dbw->update( + 'oldimage', + array( + 'oi_name' => $this->newName, + 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($this->newName) ), + ), + array( 'oi_name' => $this->oldName ), + __METHOD__ + ); + $affected = $dbw->affectedRows(); + $total = $this->oldCount; + $status->successCount += $affected; + $status->failCount += $total - $affected; + + return $status; + } + + /* + * Generate triplets for FSRepo::storeBatch(). + */ + function getMoveTriplets() { + $moves = array_merge( array( $this->cur ), $this->olds ); + $triplets = array(); // The format is: (srcUrl, destZone, destUrl) + foreach( $moves as $move ) { + // $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->name}: {$srcUrl} :: public :: {$move[1]}" ); + } + return $triplets; + } +} diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index a259bd48..90b198c8 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -2,10 +2,13 @@ /** * A repository that stores files in the local filesystem and registers them * in the wiki's own database. This is the most commonly used repository class. + * @ingroup FileRepo */ class LocalRepo extends FSRepo { var $fileFactory = array( 'LocalFile', 'newFromTitle' ); var $oldFileFactory = array( 'OldLocalFile', 'newFromTitle' ); + var $fileFromRowFactory = array( 'LocalFile', 'newFromRow' ); + var $oldFileFromRowFactory = array( 'OldLocalFile', 'newFromRow' ); function getSlaveDB() { return wfGetDB( DB_SLAVE ); @@ -15,24 +18,28 @@ class LocalRepo extends FSRepo { return wfGetDB( DB_MASTER ); } + function getMemcKey( $key ) { + return wfWikiID( $this->getSlaveDB() ) . ":{$key}"; + } + function newFileFromRow( $row ) { if ( isset( $row->img_name ) ) { - return LocalFile::newFromRow( $row, $this ); + return call_user_func( $this->fileFromRowFactory, $row, $this ); } elseif ( isset( $row->oi_name ) ) { - return OldLocalFile::newFromRow( $row, $this ); + return call_user_func( $this->oldFileFromRowFactory, $row, $this ); } else { throw new MWException( __METHOD__.': invalid row' ); } } - + function newFromArchiveName( $title, $archiveName ) { return OldLocalFile::newFromArchiveName( $title, $this, $archiveName ); } /** - * Delete files in the deleted directory if they are not referenced in the - * filearchive table. This needs to be done in the repo because it needs to - * interleave database locks with file operations, which is potentially a + * Delete files in the deleted directory if they are not referenced in the + * 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. * @return FileRepoStatus */ @@ -45,9 +52,19 @@ class LocalRepo extends FSRepo { $hashPath = $this->getDeletedHashPath( $key ); $path = "$root/$hashPath$key"; $dbw->begin(); - $inuse = $dbw->selectField( 'filearchive', '1', + $inuse = $dbw->selectField( 'filearchive', '1', 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 ); + $ext = File::normalizeExtension($ext); + $inuse = $dbw->selectField( 'oldimage', '1', + array( 'oi_sha1' => $sha1, + "oi_archive_name LIKE '%.{$ext}'", + 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ), + __METHOD__, array( 'FOR UPDATE' ) ); + } if ( !$inuse ) { wfDebug( __METHOD__ . ": deleting $key\n" ); if ( !@unlink( $path ) ) { @@ -67,7 +84,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. */ - private function getArticleID( $title ) { + protected function getArticleID( $title ) { if( !$title instanceof Title ) { return 0; } @@ -85,17 +102,26 @@ class LocalRepo extends FSRepo { } function checkRedirect( $title ) { - global $wgFileRedirects; - if( !$wgFileRedirects ) { - return false; - } + global $wgMemc; + if( is_string( $title ) ) { + $title = Title::newFromTitle( $title ); + } if( $title instanceof Title && $title->getNamespace() == NS_MEDIA ) { $title = Title::makeTitle( NS_IMAGE, $title->getText() ); } - + + $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) ); + $cachedValue = $wgMemc->get( $memcKey ); + if( $cachedValue ) { + return Title::newFromDbKey( $cachedValue ); + } elseif( $cachedValue == ' ' ) { # FIXME: ugly hack, but BagOStuff caching seems to be weird and return false if !cachedValue, not only if it doesn't exist + return false; + } + $id = $this->getArticleID( $title ); if( !$id ) { + $wgMemc->set( $memcKey, " ", 9000 ); return false; } $dbr = $this->getSlaveDB(); @@ -105,9 +131,58 @@ class LocalRepo extends FSRepo { array( 'rd_from' => $id ), __METHOD__ ); + + if( $row ) $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title ); + $wgMemc->set( $memcKey, ($row ? $targetTitle->getPrefixedDBkey() : " "), 9000 ); if( !$row ) { return false; } - return Title::makeTitle( $row->rd_namespace, $row->rd_title ); + return $targetTitle; + } + + function invalidateImageRedirect( $title ) { + global $wgMemc; + $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) ); + $wgMemc->delete( $memcKey ); + } + + function findBySha1( $hash ) { + $dbr = $this->getSlaveDB(); + $res = $dbr->select( + 'image', + LocalFile::selectFields(), + array( 'img_sha1' => $hash ) + ); + + $result = array(); + while ( $row = $res->fetchObject() ) + $result[] = $this->newFileFromRow( $row ); + $res->free(); + return $result; + } + + /* + * Find many files using one query + */ + function findFiles( $titles, $flags ) { + // FIXME: Comply with $flags + // FIXME: Only accepts a $titles array where the keys are the sanitized + // file names. + + if ( count( $titles ) == 0 ) return array(); + + $dbr = $this->getSlaveDB(); + $res = $dbr->select( + 'image', + LocalFile::selectFields(), + array( 'img_name' => array_keys( $titles ) ) + ); + + $result = array(); + while ( $row = $res->fetchObject() ) { + $result[$row->img_name] = $this->newFileFromRow( $row ); + } + $res->free(); + return $result; } } diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index 87bfd3ab..fb89cebb 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -2,15 +2,15 @@ /** * File repository with no files, for performance testing + * @ingroup FileRepo */ - class NullRepo extends FileRepo { function __construct( $info ) {} - + function storeBatch( $triplets, $flags = 0 ) { return false; } - + function storeTemp( $originalName, $srcPath ) { return false; } @@ -30,5 +30,3 @@ class NullRepo extends FileRepo { return false; } } - -?> diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php index 850a8d8a..89e49c4c 100644 --- a/includes/filerepo/OldLocalFile.php +++ b/includes/filerepo/OldLocalFile.php @@ -3,7 +3,7 @@ /** * Class to represent a file in the oldimage table * - * @addtogroup FileRepo + * @ingroup FileRepo */ class OldLocalFile extends LocalFile { var $requestedTime, $archive_name; @@ -11,7 +11,10 @@ class OldLocalFile extends LocalFile { const CACHE_VERSION = 1; const MAX_CACHE_ROWS = 20; - static function newFromTitle( $title, $repo, $time ) { + static function newFromTitle( $title, $repo, $time = null ) { + # The null default value is only here to avoid an E_STRICT + if( $time === null ) + throw new MWException( __METHOD__.' got null for $time parameter' ); return new self( $title, $repo, $time, null ); } @@ -25,6 +28,45 @@ class OldLocalFile extends LocalFile { $file->loadFromRow( $row, 'oi_' ); return $file; } + + static function newFromKey( $sha1, $repo, $timestamp = false ) { + # Polymorphic function name to distinguish foreign and local fetches + $fname = get_class( $this ) . '::' . __FUNCTION__; + + $conds = array( 'oi_sha1' => $sha1 ); + if( $timestamp ) { + $conds['oi_timestamp'] = $timestamp; + } + $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), $conds, $fname ); + if( $row ) { + return self::newFromRow( $row, $repo ); + } else { + return false; + } + } + + /** + * Fields in the oldimage table + */ + static function selectFields() { + return array( + 'oi_name', + 'oi_archive_name', + 'oi_size', + 'oi_width', + 'oi_height', + 'oi_metadata', + 'oi_bits', + 'oi_media_type', + 'oi_major_mime', + 'oi_minor_mime', + 'oi_description', + 'oi_user', + 'oi_user_text', + 'oi_timestamp', + 'oi_sha1', + ); + } /** * @param Title $title @@ -42,8 +84,7 @@ class OldLocalFile extends LocalFile { } function getCacheKey() { - $hashedName = md5($this->getName()); - return wfMemcKey( 'oldfile', $hashedName ); + return false; } function getArchiveName() { @@ -57,103 +98,8 @@ class OldLocalFile extends LocalFile { return true; } - /** - * Try to load file metadata from memcached. Returns true on success. - */ - function loadFromCache() { - global $wgMemc; - wfProfileIn( __METHOD__ ); - $this->dataLoaded = false; - $key = $this->getCacheKey(); - if ( !$key ) { - return false; - } - $oldImages = $wgMemc->get( $key ); - - if ( isset( $oldImages['version'] ) && $oldImages['version'] == self::CACHE_VERSION ) { - unset( $oldImages['version'] ); - $more = isset( $oldImages['more'] ); - unset( $oldImages['more'] ); - $found = false; - if ( is_null( $this->requestedTime ) ) { - foreach ( $oldImages as $timestamp => $info ) { - if ( $info['archive_name'] == $this->archive_name ) { - $found = true; - break; - } - } - } else { - krsort( $oldImages ); - foreach ( $oldImages as $timestamp => $info ) { - if ( $timestamp <= $this->requestedTime ) { - $found = true; - break; - } - } - } - if ( $found ) { - wfDebug( "Pulling file metadata from cache key {$key}[{$timestamp}]\n" ); - $this->dataLoaded = true; - $this->fileExists = true; - foreach ( $info as $name => $value ) { - $this->$name = $value; - } - } elseif ( $more ) { - wfDebug( "Cache key was truncated, oldimage row might be found in the database\n" ); - } else { - wfDebug( "Image did not exist at the specified time.\n" ); - $this->fileExists = false; - $this->dataLoaded = true; - } - } - - if ( $this->dataLoaded ) { - wfIncrStats( 'image_cache_hit' ); - } else { - wfIncrStats( 'image_cache_miss' ); - } - - wfProfileOut( __METHOD__ ); - return $this->dataLoaded; - } - - function saveToCache() { - // If a timestamp was specified, cache the entire history of the image (up to MAX_CACHE_ROWS). - if ( is_null( $this->requestedTime ) ) { - return; - } - // This is expensive, so we only do it if $wgMemc is real - global $wgMemc; - if ( $wgMemc instanceof FakeMemcachedClient ) { - return; - } - $key = $this->getCacheKey(); - if ( !$key ) { - return; - } - wfProfileIn( __METHOD__ ); - - $dbr = $this->repo->getSlaveDB(); - $res = $dbr->select( 'oldimage', $this->getCacheFields( 'oi_' ), - array( 'oi_name' => $this->getName() ), __METHOD__, - array( - 'LIMIT' => self::MAX_CACHE_ROWS + 1, - 'ORDER BY' => 'oi_timestamp DESC', - )); - $cache = array( 'version' => self::CACHE_VERSION ); - $numRows = $dbr->numRows( $res ); - if ( $numRows > self::MAX_CACHE_ROWS ) { - $cache['more'] = true; - $numRows--; - } - for ( $i = 0; $i < $numRows; $i++ ) { - $row = $dbr->fetchObject( $res ); - $decoded = $this->decodeRow( $row, 'oi_' ); - $cache[$row->oi_timestamp] = $decoded; - } - $dbr->freeResult( $res ); - $wgMemc->set( $key, $cache, 7*86400 /* 1 week */ ); - wfProfileOut( __METHOD__ ); + function isVisible() { + return $this->exists() && !$this->isDeleted(File::DELETED_FILE); } function loadFromDB() { @@ -164,7 +110,7 @@ class OldLocalFile extends LocalFile { if ( is_null( $this->requestedTime ) ) { $conds['oi_archive_name'] = $this->archive_name; } else { - $conds[] = 'oi_timestamp <= ' . $dbr->addQuotes( $this->requestedTime ); + $conds[] = 'oi_timestamp = ' . $dbr->addQuotes( $dbr->timestamp( $this->requestedTime ) ); } $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), $conds, __METHOD__, array( 'ORDER BY' => 'oi_timestamp DESC' ) ); @@ -179,10 +125,7 @@ class OldLocalFile extends LocalFile { function getCacheFields( $prefix = 'img_' ) { $fields = parent::getCacheFields( $prefix ); $fields[] = $prefix . 'archive_name'; - - // XXX: Temporary hack before schema update - //$fields = array_diff( $fields, array( - // 'oi_media_type', 'oi_major_mime', 'oi_minor_mime', 'oi_metadata' ) ); + $fields[] = $prefix . 'deleted'; return $fields; } @@ -193,11 +136,11 @@ class OldLocalFile extends LocalFile { function getUrlRel() { return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() ); } - + function upgradeRow() { wfProfileIn( __METHOD__ ); $this->loadFromFile(); - + # Don't destroy file info of missing files if ( !$this->fileExists ) { wfDebug( __METHOD__.": file does not exist, aborting\n" ); @@ -219,14 +162,39 @@ class OldLocalFile extends LocalFile { 'oi_minor_mime' => $minor, 'oi_metadata' => $this->metadata, 'oi_sha1' => $this->sha1, - ), array( - 'oi_name' => $this->getName(), + ), array( + 'oi_name' => $this->getName(), 'oi_archive_name' => $this->archive_name ), __METHOD__ ); wfProfileOut( __METHOD__ ); } -} - + /** + * int $field one of DELETED_* bitfield constants + * for file or revision rows + * @return bool + */ + function isDeleted( $field ) { + 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 int $field + * @return bool + */ + function userCan( $field ) { + if( isset($this->deleted) && ($this->deleted & $field) == $field ) { + global $wgUser; + $permission = ( $this->deleted & File::DELETED_RESTRICTED ) == File::DELETED_RESTRICTED + ? 'suppressrevision' + : 'deleterevision'; + wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" ); + return $wgUser->isAllowed( $permission ); + } else { + return true; + } + } +} diff --git a/includes/filerepo/README b/includes/filerepo/README index 03cb8b3b..d3aea9f0 100644 --- a/includes/filerepo/README +++ b/includes/filerepo/README @@ -1,8 +1,8 @@ Some quick notes on the file/repository architecture. -Functionality is, as always, driven by data model. +Functionality is, as always, driven by data model. -* The repository object stores configuration information about a file storage +* The repository object stores configuration information about a file storage method. * The file object is a process-local cache of information about a particular @@ -28,14 +28,14 @@ even entire classes, between repositories. These rules alone still do lead to some ambiguity -- it may not be clear whether to implement some functionality in a repository function with a filename -parameter, or in the file object itself. +parameter, or in the file object itself. -So we introduce the following rule: the file subclass is smarter than the +So we introduce the following rule: the file subclass is smarter than the repository subclass. The repository should in general provide a minimal API -needed to access the storage backend efficiently. +needed to access the storage backend efficiently. -In particular, note that I have not implemented any database access in -LocalRepo.php. LocalRepo provides only file access, and LocalFile provides +In particular, note that I have not implemented any database access in +LocalRepo.php. LocalRepo provides only file access, and LocalFile provides database access and higher-level functions such as cache management. Tim Starling, June 2007 diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index b0e1d782..7cb837b3 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -1,8 +1,14 @@ reposInitialised ) { $this->initialiseRepos(); } - $image = $this->localRepo->findFile( $title, $time ); + $image = $this->localRepo->findFile( $title, $time, $flags ); if ( $image ) { return $image; } foreach ( $this->foreignRepos as $repo ) { - $image = $repo->findFile( $title, $time ); + $image = $repo->findFile( $title, $time, $flags ); if ( $image ) { return $image; } } return false; } + function findFiles( $titles, $flags = 0 ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $titleObjs = array(); + foreach ( $titles as $title ) { + if ( !( $title instanceof Title ) ) + $title = Title::makeTitleSafe( NS_IMAGE, $title ); + $titleObjs[$title->getDBkey()] = $title; + } + + $images = $this->localRepo->findFiles( $titleObjs, $flags ); + + foreach ( $this->foreignRepos as $repo ) { + // Remove found files from $titleObjs + foreach ( $images as $name => $image ) + if ( isset( $titleObjs[$name] ) ) + unset( $titleObjs[$name] ); + + $images = array_merge( $images, $repo->findFiles( $titleObjs, $flags ) ); + } + return $images; + } /** * Interface for FileRepo::checkRedirect() @@ -96,6 +127,17 @@ class RepoGroup { } return false; } + + function findBySha1( $hash ) { + if ( !$this->reposInitialised ) { + $this->initialiseRepos(); + } + + $result = $this->localRepo->findBySha1( $hash ); + foreach ( $this->foreignRepos as $repo ) + $result = array_merge( $result, $repo->findBySha1( $hash ) ); + return $result; + } /** * Get the repo instance with a given key. @@ -134,6 +176,20 @@ class RepoGroup { return $this->getRepo( 'local' ); } + function forEachForeignRepo( $callback, $params = array() ) { + foreach( $this->foreignRepos as $repo ) { + $args = array_merge( array( $repo ), $params ); + if( call_user_func_array( $callback, $args ) ) { + return true; + } + } + return false; + } + + function hasForeignRepos() { + return !empty( $this->foreignRepos ); + } + /** * Initialise the $repos array */ @@ -187,5 +243,3 @@ class RepoGroup { } } } - - diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php index 419c61f6..c687ef6e 100644 --- a/includes/filerepo/UnregisteredLocalFile.php +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -1,14 +1,16 @@