From 9db190c7e736ec8d063187d4241b59feaf7dc2d1 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 22 Jun 2011 11:28:20 +0200 Subject: update to MediaWiki 1.17.0 --- includes/filerepo/ArchivedFile.php | 18 +- includes/filerepo/FSRepo.php | 30 +- includes/filerepo/File.php | 128 +++++---- includes/filerepo/FileRepo.php | 173 +++++++----- includes/filerepo/FileRepoStatus.php | 6 + includes/filerepo/ForeignAPIFile.php | 83 ++++-- includes/filerepo/ForeignAPIRepo.php | 258 +++++++++++++----- includes/filerepo/ForeignDBFile.php | 8 + includes/filerepo/ForeignDBRepo.php | 23 +- includes/filerepo/ForeignDBViaLBRepo.php | 7 + includes/filerepo/Image.php | 16 +- includes/filerepo/LocalFile.php | 409 ++++++++++++++++++++-------- includes/filerepo/LocalRepo.php | 20 +- includes/filerepo/NullRepo.php | 6 + includes/filerepo/OldLocalFile.php | 29 +- includes/filerepo/RepoGroup.php | 31 ++- includes/filerepo/UnregisteredLocalFile.php | 8 +- 17 files changed, 885 insertions(+), 368 deletions(-) (limited to 'includes/filerepo') diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php index ffc06303..ecc09978 100644 --- a/includes/filerepo/ArchivedFile.php +++ b/includes/filerepo/ArchivedFile.php @@ -1,10 +1,17 @@ id = -1; $this->title = false; $this->name = false; @@ -140,7 +147,6 @@ class ArchivedFile $this->deleted = $row->fa_deleted; } else { throw new MWException( 'This title does not correspond to an image page.' ); - return; } $this->dataLoaded = true; $this->exists = true; @@ -219,7 +225,7 @@ class ArchivedFile * Return the FileStore storage group */ public function getGroup() { - return $file->group; + return $this->group; } /** diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php index 0dd9d0f7..e2251b2b 100644 --- a/includes/filerepo/FSRepo.php +++ b/includes/filerepo/FSRepo.php @@ -1,4 +1,10 @@ name}/temp"; @@ -343,8 +349,8 @@ class FSRepo extends FileRepo { /** * Publish a batch of files - * @param array $triplets (source,dest,archive) triplets as per publish() - * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * @param $triplets Array: (source,dest,archive) triplets as per publish() + * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source files should be deleted if possible */ function publishBatch( $triplets, $flags = 0 ) { @@ -454,7 +460,7 @@ class FSRepo extends FileRepo { * 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 $sourceDestPairs Array of source/destination pairs. Each element * is a two-element array containing the source file path relative to the * public root in the first element, and the archive file path relative * to the deleted zone root in the second element. @@ -615,7 +621,7 @@ class FSRepo extends FileRepo { /** * Chmod a file, supressing the warnings. - * @param String $path The path to change + * @param $path String: the path to change */ protected function chmod( $path ) { wfSuppressWarnings(); diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php index d79a1661..192e8c8a 100644 --- a/includes/filerepo/File.php +++ b/includes/filerepo/File.php @@ -1,4 +1,10 @@ getUrl() ); @@ -259,6 +266,19 @@ abstract class File { } } + /** + * Return true if the file is vectorized + */ + public function isVectorized() { + $handler = $this->getHandler(); + if ( $handler ) { + return $handler->isVectorized( $this ); + } else { + return false; + } + } + + /** * Get handler-specific metadata * Overridden by LocalFile, UnregisteredLocalFile @@ -437,26 +457,21 @@ abstract class File { /** * Get a ThumbnailImage which is the same size as the source */ - function getUnscaledThumb( $page = false ) { + function getUnscaledThumb( $handlerParams = array() ) { + $hp =& $handlerParams; + $page = isset( $hp['page'] ) ? $hp['page'] : false; $width = $this->getWidth( $page ); if ( !$width ) { return $this->iconThumb(); } - if ( $page ) { - $params = array( - 'page' => $page, - 'width' => $this->getWidth( $page ) - ); - } else { - $params = array( 'width' => $this->getWidth() ); - } - return $this->transform( $params ); + $hp['width'] = $width; + return $this->transform( $hp ); } /** * Return the file name of a thumbnail with the specified parameters * - * @param array $params Handler-specific parameters + * @param $params Array: handler-specific parameters * @private -ish */ function thumbName( $params ) { @@ -464,7 +479,7 @@ abstract class File { return null; } $extension = $this->getExtension(); - list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() ); + list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType(), $params ); $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName(); if ( $thumbExt != $extension ) { $thumbName .= ".$thumbExt"; @@ -484,8 +499,8 @@ abstract class File { * specified, the generated image will be no bigger than width x height, * and will also have correct aspect ratio. * - * @param integer $width maximum width of the generated thumbnail - * @param integer $height maximum height of the image (optional) + * @param $width Integer: maximum width of the generated thumbnail + * @param $height Integer: maximum height of the image (optional) */ public function createThumb( $width, $height = -1 ) { $params = array( 'width' => $width ); @@ -500,19 +515,20 @@ abstract class File { /** * As createThumb, but returns a ThumbnailImage object. This can * provide access to the actual file, the real size of the thumb, - * and can produce a convenient tag for you. + * and can produce a convenient \ tag for you. * * For non-image formats, this may return a filetype-specific icon. * - * @param integer $width maximum width of the generated thumbnail - * @param integer $height maximum height of the image (optional) - * @param boolean $render Deprecated + * @param $width Integer: maximum width of the generated thumbnail + * @param $height Integer: maximum height of the image (optional) + * @param $render Integer: Deprecated * * @return ThumbnailImage or null on failure * * @deprecated use transform() */ public function getThumbnail( $width, $height=-1, $render = true ) { + wfDeprecated( __METHOD__ ); $params = array( 'width' => $width ); if ( $height != -1 ) { $params['height'] = $height; @@ -523,10 +539,10 @@ abstract class File { /** * Transform a media file * - * @param array $params An associative array of handler-specific parameters. Typical - * keys are width, height and page. - * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering - * @return MediaTransformOutput + * @param $params Array: an associative array of handler-specific parameters. + * Typical keys are width, height and page. + * @param $flags Integer: a bitfield, may contain self::RENDER_NOW to force rendering + * @return MediaTransformOutput | false */ function transform( $params, $flags = 0 ) { global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgServer; @@ -560,7 +576,7 @@ abstract class File { $thumbPath = $this->getThumbPath( $thumbName ); $thumbUrl = $this->getThumbUrl( $thumbName ); - if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { + if ( $this->repo && $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) { $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params ); break; } @@ -842,19 +858,18 @@ abstract class File { /** * 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. + * destination, move it to an archive. Returns a FileRepoStatus object with + * the archive name in the "value" member on success. * * The archive name should be passed through to recordUpload for database * registration. * - * @param string $sourcePath Local filesystem path to the source image - * @param integer $flags A bitwise combination of: + * @param $srcPath String: local filesystem path to the source image + * @param $flags Integer: a bitwise combination of: * File::DELETE_SOURCE Delete the source file, i.e. move * rather than copy - * @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 FileRepoStatus object. On success, the value member contains the + * archive name, or an empty string if it was a new file. * * STUB * Overridden by LocalFile @@ -872,6 +887,7 @@ abstract class File { * @deprecated Use HTMLCacheUpdate, this function uses too much memory */ function getLinksTo( $options = array() ) { + wfDeprecated( __METHOD__ ); wfProfileIn( __METHOD__ ); // Note: use local DB not repo DB, we want to know local links @@ -884,21 +900,21 @@ abstract class File { $encName = $db->addQuotes( $this->getName() ); $res = $db->select( array( 'page', 'imagelinks'), - array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect' ), - array( 'page_id' => 'il_from', 'il_to' => $encName ), + array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ), + array( 'page_id=il_from', 'il_to' => $encName ), __METHOD__, $options ); $retVal = array(); if ( $db->numRows( $res ) ) { - while ( $row = $db->fetchObject( $res ) ) { - if ( $titleObj = Title::newFromRow( $row ) ) { - $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect ); + foreach ( $res as $row ) { + $titleObj = Title::newFromRow( $row ); + if ( $titleObj ) { + $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect, $row->page_latest ); $retVal[] = $titleObj; } } } - $db->freeResult( $res ); wfProfileOut( __METHOD__ ); return $retVal; } @@ -916,7 +932,8 @@ abstract class File { * @return bool */ function isLocal() { - return $this->getRepoName() == 'local'; + $repo = $this->getRepo(); + return $repo && $repo->isLocal(); } /** @@ -992,8 +1009,8 @@ abstract class File { * * Cache purging is done; logging is caller's responsibility. * - * @param $reason - * @param $suppress, hide content from sysops? + * @param $reason String + * @param $suppress Boolean: hide content from sysops? * @return true on success, false on some kind of failure * STUB * Overridden by LocalFile @@ -1010,7 +1027,7 @@ 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? + * @param $unsuppress remove restrictions on content upon restoration? * @return the number of file revisions restored if successful, * or false on failure * STUB @@ -1032,7 +1049,7 @@ abstract class File { } /** - * Returns the number of pages of a multipage document, or NULL for + * Returns the number of pages of a multipage document, or false for * documents which aren't multipage documents */ function pageCount() { @@ -1059,11 +1076,11 @@ abstract class File { } /** - * 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 - * @return array + * @param $fileName String: The filename + * @return Array */ function getImageSize( $fileName ) { if ( !$this->getHandler() ) { @@ -1156,8 +1173,8 @@ abstract class File { * Determine if the current user is allowed to view a particular * field of this file, if it's marked as deleted. * STUB - * @param int $field - * @return bool + * @param $field Integer + * @return Boolean */ function userCan( $field ) { return true; @@ -1166,9 +1183,9 @@ 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. - * Set it to false to ignore the extension. + * @param $path String: absolute local filesystem path + * @param $ext Mixed: the file extension, or true to extract it from the filename. + * Set it to false to ignore the extension. */ static function getPropsFromPath( $path, $ext = true ) { wfProfileIn( __METHOD__ ); @@ -1180,7 +1197,16 @@ abstract class File { if ( $info['fileExists'] ) { $magic = MimeMagic::singleton(); - $info['mime'] = $magic->guessMimeType( $path, $ext ); + if ( $ext === true ) { + $i = strrpos( $path, '.' ); + $ext = strtolower( $i ? substr( $path, $i + 1 ) : '' ); + } + + # mime type according to file contents + $info['file-mime'] = $magic->guessMimeType( $path, false ); + # logical mime type + $info['mime'] = $magic->improveTypeFromExtension( $info['file-mime'], $ext ); + list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] ); $info['media_type'] = $magic->getMediaType( $path, $info['mime'] ); diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index f94709b3..ff73a73c 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -1,8 +1,15 @@ initialCapital = MWNamespace::isCapitalized( NS_FILE ); foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription', 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', - 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl' ) as $var ) + 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl', 'scriptExtension' ) + as $var ) { if ( isset( $info[$var] ) ) { $this->$var = $info[$var]; @@ -49,12 +58,13 @@ 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 was uploaded. - * If this is specified, the returned object will be an - * instance of the repository's old file class instead of - * a current file. Repositories not supporting version - * control should return false if this parameter is set. + * + * @param $title Mixed: Title object or string + * @param $time Mixed: Time at which the image was uploaded. + * If this is specified, the returned object will be an + * instance of the repository's old file class instead of a + * current file. Repositories not supporting version control + * should return false if this parameter is set. */ function newFile( $title, $time = false ) { if ( !($title instanceof Title) ) { @@ -79,7 +89,7 @@ abstract class FileRepo { * 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 $title Mixed: Title object or string * @param $options Associative array of options: * time: requested time for an archived image, or false for the * current version. An image object will be returned which was @@ -144,7 +154,7 @@ abstract class FileRepo { /* * Find many files at once. - * @param array $items, an array of titles, or an array of findFile() options with + * @param $items An array of titles, or an array of findFile() options with * the "title" option giving the title. Example: * * $findItem = array( 'title' => $title, 'private' => true ); @@ -153,7 +163,7 @@ abstract class FileRepo { */ function findFiles( $items ) { $result = array(); - foreach ( $items as $index => $item ) { + foreach ( $items as $item ) { if ( is_array( $item ) ) { $title = $item['title']; $options = $item; @@ -163,31 +173,33 @@ abstract class FileRepo { $options = array(); } $file = $this->findFile( $title, $options ); - if ( $file ) + 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. + * @param $sha1 Mixed: SHA-1 key + * @param $time Mixed: time at which the image was uploaded. + * If this is specified, the returned object will be an + * of the repository's old file class instead of a current + * file. Repositories not supporting version control should + * return false if this parameter is set. */ function newFileFromKey( $sha1, $time = false ) { if ( $time ) { if ( $this->oldFileFactoryKey ) { return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time ); - } else { - return false; } } else { - return call_user_func( $this->fileFactoryKey, $sha1, $this ); + if ( $this->fileFactoryKey ) { + return call_user_func( $this->fileFactoryKey, $sha1, $this ); + } } + return false; } /** @@ -195,8 +207,8 @@ abstract class FileRepo { * Returns false if the file does not exist. Repositories not supporting * version control should return false if the time is specified. * - * @param string $sha1 string - * @param array $options Option array, same as findFile(). + * @param $sha1 String + * @param $options Option array, same as findFile(). */ function findFileFromKey( $sha1, $options = array() ) { if ( !is_array( $options ) ) { @@ -217,7 +229,7 @@ abstract class FileRepo { # Now try an old version of the file if ( $time !== false ) { $img = $this->newFileFromKey( $sha1, $time ); - if ( $img->exists() ) { + if ( $img && $img->exists() ) { if ( !$img->isDeleted(File::DELETED_FILE) ) { return $img; } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) { @@ -237,7 +249,7 @@ abstract class FileRepo { /** * Get the URL corresponding to one of the four basic zones - * @param String $zone One of: public, deleted, temp, thumb + * @param $zone String: one of: public, deleted, temp, thumb * @return String or false */ function getZoneUrl( $zone ) { @@ -255,7 +267,6 @@ abstract class FileRepo { * Get the name of an image from its title object */ function getNameFromTitle( $title ) { - global $wgCapitalLinks; if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) { global $wgContLang; $name = $title->getUserCaseDBKey(); @@ -295,6 +306,18 @@ abstract class FileRepo { function getName() { return $this->name; } + + /** + * Make an url to this repo + * + * @param $query mixed Query string to append + * @param $entry string Entry point; defaults to index + * @return string + */ + function makeUrl( $query = '', $entry = 'index' ) { + $ext = isset( $this->scriptExtension ) ? $this->scriptExtension : '.php'; + return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}{$ext}", $query ); + } /** * Get the URL of an image description page. May return false if it is @@ -325,8 +348,7 @@ abstract class FileRepo { # We use "Image:" as the canonical namespace for # compatibility across all MediaWiki versions, # and just sort of hope index.php is right. ;) - return $this->scriptDirUrl . - "/index.php?title=Image:$encName"; + return $this->makeUrl( "title=Image:$encName" ); } return false; } @@ -336,8 +358,8 @@ abstract class FileRepo { * 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(). - * @param string $name Name of image to fetch - * @param string $lang Language to fetch it in, if any. + * @param $name String: name of image to fetch + * @param $lang String: language to fetch it in, if any. */ function getDescriptionRenderUrl( $name, $lang = null ) { $query = 'action=render'; @@ -345,9 +367,10 @@ abstract class FileRepo { $query .= '&uselang=' . $lang; } if ( isset( $this->scriptDirUrl ) ) { - return $this->scriptDirUrl . '/index.php?title=' . + return $this->makeUrl( + 'title=' . wfUrlencode( 'Image:' . $name ) . - "&$query"; + "&$query" ); } else { $descUrl = $this->getDescriptionUrl( $name ); if ( $descUrl ) { @@ -357,14 +380,25 @@ abstract class FileRepo { } } } + + /** + * Get the URL of the stylesheet to apply to description pages + * @return string + */ + function getDescriptionStylesheetUrl() { + if ( $this->scriptDirUrl ) { + return $this->makeUrl( 'title=MediaWiki:Filepage.css&' . + wfArrayToCGI( Skin::getDynamicStylesheetQuery() ) ); + } + } /** * Store a file to a given destination. * - * @param string $srcPath Source path or virtual URL - * @param string $dstZone Destination zone - * @param string $dstRel Destination relative path - * @param integer $flags Bitwise combination of the following flags: + * @param $srcPath String: source path or virtual URL + * @param $dstZone String: destination zone + * @param $dstRel String: destination relative path + * @param $flags Integer: bitwise combination of the following flags: * self::DELETE_SOURCE Delete the source file after upload * self::OVERWRITE Overwrite an existing destination file instead of failing * self::OVERWRITE_SAME Overwrite the file if the destination exists and has the @@ -382,8 +416,8 @@ abstract class FileRepo { /** * Store a batch of files * - * @param array $triplets (src,zone,dest) triplets as per store() - * @param integer $flags Flags as per store + * @param $triplets Array: (src,zone,dest) triplets as per store() + * @param $flags Integer: flags as per store */ abstract function storeBatch( $triplets, $flags = 0 ); @@ -391,18 +425,18 @@ 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 $originalName String: 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. + * @param $srcPath String: the current location of the file. */ abstract function storeTemp( $originalName, $srcPath ); /** * Append the contents of the source path to the given file. - * @param $srcPath string location of the source file - * @param $toAppendPath string path to append to. - * @param $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * @param $srcPath String: location of the source file + * @param $toAppendPath String: path to append to. + * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source file should be deleted if possible * @return mixed Status or false */ @@ -410,8 +444,8 @@ abstract class FileRepo { /** * Remove a temporary file or mark it for garbage collection - * @param string $virtualUrl The virtual URL returned by storeTemp - * @return boolean True on success, false on failure + * @param $virtualUrl String: the virtual URL returned by storeTemp + * @return Boolean: true on success, false on failure * STUB */ function freeTemp( $virtualUrl ) { @@ -425,11 +459,11 @@ abstract class FileRepo { * Returns a FileRepoStatus object. On success, the value contains "new" or * "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 - * @param string $archiveRel The relative path where the existing file is to + * @param $srcPath String: the source path or URL + * @param $dstRel String: the destination relative path + * @param $archiveRel String: rhe relative path where the existing file is to * be archived, if there is one. Relative to the public zone root. - * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source file should be deleted if possible */ function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { @@ -447,8 +481,8 @@ abstract class FileRepo { /** * Publish a batch of files - * @param array $triplets (source,dest,archive) triplets as per publish() - * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate + * @param $triplets Array: (source,dest,archive) triplets as per publish() + * @param $flags Integer: bitfield, may be FileRepo::DELETE_SOURCE to indicate * that the source files should be deleted if possible */ abstract function publishBatch( $triplets, $flags = 0 ); @@ -461,8 +495,8 @@ abstract class FileRepo { /** * Checks existence of an array of files. * - * @param array $files URLs (or paths) of files to check - * @param integer $flags Bitwise combination of the following flags: + * @param $files Array: URLs (or paths) of files to check + * @param $flags Integer: bitwise combination of the following flags: * self::FILES_ONLY Mark file as existing only if it is a file (not directory) * @return Either array of files and existence flags, or false */ @@ -478,7 +512,7 @@ abstract class FileRepo { * assumes a naming scheme in the deleted zone based on content hash, as * opposed to the public zone which is assumed to be unique. * - * @param array $sourceDestPairs Array of source/destination pairs. Each element + * @param $sourceDestPairs Array of source/destination pairs. Each element * is a two-element array containing the source file path relative to the * public root in the first element, and the archive file path relative * to the deleted zone root in the second element. @@ -490,10 +524,10 @@ abstract class FileRepo { * Move a file to the deletion archive. * 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 $srcRel Mixed: relative path for the file to be deleted + * @param $archiveRel Mixed: relative path for the archive location. * Relative to a private archive directory. - * @return WikiError object (wikitext-formatted), or true for success + * @return FileRepoStatus object */ function delete( $srcRel, $archiveRel ) { return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) ); @@ -589,7 +623,7 @@ abstract class FileRepo { * title object. If not, return false. * STUB * - * @param Title $title Title of image + * @param $title Title of image */ function checkRedirect( $title ) { return false; @@ -600,7 +634,7 @@ abstract class FileRepo { * Doesn't do anything for repositories that don't support image redirects. * * STUB - * @param Title $title Title of image + * @param $title Title of image */ function invalidateImageRedirect( $title ) {} @@ -620,7 +654,7 @@ abstract class FileRepo { */ public function getDisplayName() { // We don't name our own repo, return nothing - if ( $this->name == 'local' ) { + if ( $this->isLocal() ) { return null; } // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true @@ -631,6 +665,16 @@ abstract class FileRepo { return wfMsg( 'shared-repo' ); } + /** + * Returns true if this the local file repository. + * + * @return bool + */ + function isLocal() { + return $this->getName() == 'local'; + } + + /** * Get a key on the primary cache for this repository. * Returns false if the repository's cache is not accessible at this site. @@ -652,4 +696,11 @@ abstract class FileRepo { array_unshift( $args, 'filerepo', $this->getName() ); return call_user_func_array( 'wfMemcKey', $args ); } + + /** + * Get an UploadStash associated with this repo. + */ + function getUploadStash() { + return new UploadStash( $this ); + } } diff --git a/includes/filerepo/FileRepoStatus.php b/includes/filerepo/FileRepoStatus.php index 63460fa8..161284c0 100644 --- a/includes/filerepo/FileRepoStatus.php +++ b/includes/filerepo/FileRepoStatus.php @@ -1,4 +1,10 @@ mInfo = $info; $this->mExists = $exists; } - + + /** + * @static + * @param $title Title + * @param $repo ForeignApiRepo + * @return ForeignAPIFile|null + */ static function newFromTitle( $title, $repo ) { - $info = $repo->getImageInfo( $title ); + $data = $repo->fetchImageQuery( array( + 'titles' => 'File:' . $title->getDBKey(), + 'iiprop' => self::getProps(), + 'prop' => 'imageinfo' ) ); + + $info = $repo->getImageInfo( $data ); + if( $info ) { - return new ForeignAPIFile( $title, $repo, $info, true ); + $lastRedirect = isset( $data['query']['redirects'] ) + ? count( $data['query']['redirects'] ) - 1 + : -1; + if( $lastRedirect >= 0 ) { + $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to']); + $img = new ForeignAPIFile( $newtitle, $repo, $info, true ); + if( $img ) { + $img->redirectedFrom( $title->getDBkey() ); + } + } else { + $img = new ForeignAPIFile( $title, $repo, $info, true ); + } + return $img; } else { return null; } } + /** + * Get the property string for iiprop and aiprop + */ + static function getProps() { + return 'timestamp|user|comment|url|size|sha1|metadata|mime'; + } + // Dummy functions... public function exists() { return $this->mExists; @@ -40,10 +77,10 @@ class ForeignAPIFile extends File { return parent::transform( $params, $flags ); } $thumbUrl = $this->repo->getThumbUrlFromCache( - $this->getName(), - isset( $params['width'] ) ? $params['width'] : -1, - isset( $params['height'] ) ? $params['height'] : -1 ); - return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );; + $this->getName(), + isset( $params['width'] ) ? $params['width'] : -1, + isset( $params['height'] ) ? $params['height'] : -1 ); + return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params ); } // Info we can get from API... @@ -74,27 +111,33 @@ class ForeignAPIFile extends File { } public function getSize() { - return intval( @$this->mInfo['size'] ); + return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null; } public function getUrl() { - return strval( @$this->mInfo['url'] ); + return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null; } public function getUser( $method='text' ) { - return strval( @$this->mInfo['user'] ); + return isset( $this->mInfo['user'] ) ? strval( $this->mInfo['user'] ) : null; } public function getDescription() { - return strval( @$this->mInfo['comment'] ); + return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null; } function getSha1() { - return wfBaseConvert( strval( @$this->mInfo['sha1'] ), 16, 36, 31 ); + return isset( $this->mInfo['sha1'] ) ? + wfBaseConvert( strval( $this->mInfo['sha1'] ), 16, 36, 31 ) : + null; } function getTimestamp() { - return wfTimestamp( TS_MW, strval( @$this->mInfo['timestamp'] ) ); + return wfTimestamp( TS_MW, + isset( $this->mInfo['timestamp'] ) ? + strval( $this->mInfo['timestamp'] ) : + null + ); } function getMimeType() { @@ -122,15 +165,13 @@ class ForeignAPIFile extends File { */ function getThumbPath( $suffix = '' ) { if ( $this->repo->canCacheThumbs() ) { - global $wgUploadDirectory; - $path = $wgUploadDirectory . '/thumb/' . $this->getHashPath( $this->getName() ); + $path = $this->repo->getZonePath('thumb') . '/' . $this->getHashPath( $this->getName() ); if ( $suffix ) { $path = $path . $suffix . '/'; } return $path; - } - else { - return null; + } else { + return null; } } diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php index 264cb920..e4188d6b 100644 --- a/includes/filerepo/ForeignAPIRepo.php +++ b/includes/filerepo/ForeignAPIRepo.php @@ -1,9 +1,13 @@ mApiBase = $info['apibase']; // http://commons.wikimedia.org/w/api.php + global $wgUploadDirectory; + + // http://commons.wikimedia.org/w/api.php + $this->mApiBase = isset( $info['apibase'] ) ? $info['apibase'] : null; + $this->directory = isset( $info['directory'] ) ? $info['directory'] : $wgUploadDirectory; + if( isset( $info['apiThumbCacheExpiry'] ) ) { $this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry']; } + if( isset( $info['fileCacheExpiry'] ) ) { + $this->fileCacheExpiry = $info['fileCacheExpiry']; + } if( !$this->scriptDirUrl ) { // hack for description fetches $this->scriptDirUrl = dirname( $this->mApiBase ); @@ -41,6 +65,11 @@ class ForeignAPIRepo extends FileRepo { if( $this->canCacheThumbs() && !$this->thumbUrl ) { $this->thumbUrl = $this->url . '/thumb'; } + if ( isset( $info['thumbDir'] ) ) { + $this->thumbDir = $info['thumbDir']; + } else { + $this->thumbDir = "{$this->directory}/thumb"; + } } /** @@ -89,7 +118,7 @@ class ForeignAPIRepo extends FileRepo { } } - $results = $this->fetchImageQuery( array( 'titles' => implode( $files, '|' ), + $data = $this->fetchImageQuery( array( 'titles' => implode( $files, '|' ), 'prop' => 'imageinfo' ) ); if( isset( $data['query']['pages'] ) ) { $i = 0; @@ -98,40 +127,32 @@ class ForeignAPIRepo extends FileRepo { $i++; } } + return $results; } 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 ) { + function fetchImageQuery( $query ) { global $wgMemc; - $url = $this->mApiBase . - '?' . - wfArrayToCgi( - array_merge( $query, - array( - 'format' => 'json', - 'action' => 'query' ) ) ); + $query = array_merge( $query, + array( + 'format' => 'json', + 'action' => 'query', + 'redirects' => 'true' + ) ); + if ( $this->mApiBase ) { + $url = wfAppendQuery( $this->mApiBase, $query ); + } else { + $url = $this->makeUrl( $query, 'api' ); + } if( !isset( $this->mQueryCache[$url] ) ) { $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) ); $data = $wgMemc->get( $key ); if( !$data ) { - $data = Http::get( $url ); + $data = self::httpGet( $url ); if ( !$data ) { return null; } @@ -147,81 +168,141 @@ class ForeignAPIRepo extends FileRepo { return FormatJson::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', - 'prop' => 'imageinfo' ) ); + function getImageInfo( $data ) { + if( $data && isset( $data['query']['pages'] ) ) { + foreach( $data['query']['pages'] as $info ) { + if( isset( $info['imageinfo'][0] ) ) { + return $info['imageinfo'][0]; + } + } + } + return false; } function findBySha1( $hash ) { $results = $this->fetchImageQuery( array( 'aisha1base36' => $hash, - 'aiprop' => 'timestamp|user|comment|url|size|sha1|metadata|mime', + 'aiprop' => ForeignAPIFile::getProps(), 'list' => 'allimages', ) ); $ret = array(); if ( isset( $results['query']['allimages'] ) ) { foreach ( $results['query']['allimages'] as $img ) { + // 1.14 was broken, doesn't return name attribute + if( !isset( $img['name'] ) ) { + continue; + } $ret[] = new ForeignAPIFile( Title::makeTitle( NS_FILE, $img['name'] ), $this, $img ); } } return $ret; } - function getThumbUrl( $name, $width=-1, $height=-1 ) { - $info = $this->queryImage( array( - 'titles' => 'Image:' . $name, - 'iiprop' => 'url', + function getThumbUrl( $name, $width=-1, $height=-1, &$result=NULL ) { + $data = $this->fetchImageQuery( array( + 'titles' => 'File:' . $name, + 'iiprop' => 'url|timestamp', 'iiurlwidth' => $width, 'iiurlheight' => $height, 'prop' => 'imageinfo' ) ); - if( $info && $info['thumburl'] ) { + $info = $this->getImageInfo( $data ); + + if( $data && $info && isset( $info['thumburl'] ) ) { wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" ); + $result = $info; return $info['thumburl']; } else { return false; } } + /* + * Return the imageurl from cache if possible + * + * If the url has been requested today, get it from cache + * Otherwise retrieve remote thumb url, check for local file. + * + * @param $name String is a dbkey form of a title + * @param $width + * @param $height + */ function getThumbUrlFromCache( $name, $width, $height ) { - global $wgMemc, $wgUploadPath, $wgServer, $wgUploadDirectory; + global $wgMemc; if ( !$this->canCacheThumbs() ) { return $this->getThumbUrl( $name, $width, $height ); } - $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $name ); - if ( $thumbUrl = $wgMemc->get($key) ) { - wfDebug("Got thumb from local cache. $thumbUrl \n"); - return $thumbUrl; - } - else { - $foreignUrl = $this->getThumbUrl( $name, $width, $height ); - if( !$foreignUrl ) { - wfDebug( __METHOD__ . " Could not find thumburl\n" ); - return false; - } - $thumb = Http::get( $foreignUrl ); - if( !$thumb ) { - wfDebug( __METHOD__ . " Could not download thumb\n" ); - return false; + $sizekey = "$width:$height"; + + /* Get the array of urls that we already know */ + $knownThumbUrls = $wgMemc->get($key); + if( !$knownThumbUrls ) { + /* No knownThumbUrls for this file */ + $knownThumbUrls = array(); + } else { + if( isset( $knownThumbUrls[$sizekey] ) ) { + wfDebug("Got thumburl from local cache. {$knownThumbUrls[$sizekey]} \n"); + return $knownThumbUrls[$sizekey]; } - // We need the same filename as the remote one :) - $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) ); - $path = 'thumb/' . $this->getHashPath( $name ) . $name . "/"; - if ( !is_dir($wgUploadDirectory . '/' . $path) ) { - wfMkdirParents($wgUploadDirectory . '/' . $path); + /* This size is not yet known */ + } + + $metadata = null; + $foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata ); + + if( !$foreignUrl ) { + wfDebug( __METHOD__ . " Could not find thumburl\n" ); + return false; + } + + // We need the same filename as the remote one :) + $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) ); + if( !$this->validateFilename( $fileName ) ) { + wfDebug( __METHOD__ . " The deduced filename $fileName is not safe\n" ); + return false; + } + $localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name; + $localFilename = $localPath . "/" . $fileName; + $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . rawurlencode( $name ) . "/" . rawurlencode( $fileName ); + + if( file_exists( $localFilename ) && isset( $metadata['timestamp'] ) ) { + wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" ); + $modified = filemtime( $localFilename ); + $remoteModified = strtotime( $metadata['timestamp'] ); + $current = time(); + $diff = abs( $modified - $current ); + if( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) { + /* Use our current and already downloaded thumbnail */ + $knownThumbUrls["$width:$height"] = $localUrl; + $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); + return $localUrl; } - $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName; - # FIXME: Delete old thumbs that aren't being used. Maintenance script? - if( !file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb ) ) { - wfDebug( __METHOD__ . " could not write to thumb path\n" ); + /* There is a new Commons file, or existing thumbnail older than a month */ + } + $thumb = self::httpGet( $foreignUrl ); + if( !$thumb ) { + wfDebug( __METHOD__ . " Could not download thumb\n" ); + return false; + } + if ( !is_dir($localPath) ) { + if( !wfMkdirParents($localPath) ) { + wfDebug( __METHOD__ . " could not create directory $localPath for thumb\n" ); return $foreignUrl; } - $wgMemc->set( $key, $localUrl, $this->apiThumbCacheExpiry ); - wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); - return $localUrl; } + + # FIXME: Delete old thumbs that aren't being used. Maintenance script? + wfSuppressWarnings(); + if( !file_put_contents( $localFilename, $thumb ) ) { + wfRestoreWarnings(); + wfDebug( __METHOD__ . " could not write to thumb path\n" ); + return $foreignUrl; + } + wfRestoreWarnings(); + $knownThumbUrls[$sizekey] = $localUrl; + $wgMemc->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); + wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" ); + return $localUrl; } /** @@ -238,6 +319,20 @@ class ForeignAPIRepo extends FileRepo { } } + /** + * Get the local directory corresponding to one of the three basic zones + */ + function getZonePath( $zone ) { + switch ( $zone ) { + case 'public': + return $this->directory; + case 'thumb': + return $this->thumbDir; + default: + return false; + } + } + /** * Are we locally caching the thumbnails? * @return bool @@ -245,4 +340,37 @@ class ForeignAPIRepo extends FileRepo { public function canCacheThumbs() { return ( $this->apiThumbCacheExpiry > 0 ); } + + /** + * The user agent the ForeignAPIRepo will use. + */ + public static function getUserAgent() { + return Http::userAgent() . " ForeignAPIRepo/" . self::VERSION; + } + + /** + * Like a Http:get request, but with custom User-Agent. + * @see Http:get + */ + public static function httpGet( $url, $timeout = 'default', $options = array() ) { + $options['timeout'] = $timeout; + /* Http::get */ + $url = wfExpandUrl( $url ); + wfDebug( "ForeignAPIRepo: HTTP GET: $url\n" ); + $options['method'] = "GET"; + + if ( !isset( $options['timeout'] ) ) { + $options['timeout'] = 'default'; + } + + $req = MWHttpRequest::factory( $url, $options ); + $req->setUserAgent( ForeignAPIRepo::getUserAgent() ); + $status = $req->execute(); + + if ( $status->isOK() ) { + return $req->getContent(); + } else { + return false; + } + } } diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php index a24ff72b..5f04ea73 100644 --- a/includes/filerepo/ForeignDBFile.php +++ b/includes/filerepo/ForeignDBFile.php @@ -1,6 +1,14 @@ dbConn ) ) { - $class = 'Database' . ucfirst( $this->dbType ); - $this->dbConn = new $class( $this->dbServer, $this->dbUser, - $this->dbPassword, $this->dbName, false, $this->dbFlags, - $this->tablePrefix ); + $this->dbConn = DatabaseBase::newFromType( $this->dbType, + array( + 'server' => $this->dbServer, + 'user' => $this->dbUser, + 'password' => $this->dbPassword, + 'dbname' => $this->dbName, + 'flags' => $this->dbFlags, + 'tableprefix' => $this->tablePrefix + ) + ); } return $this->dbConn; } @@ -65,7 +78,7 @@ class ForeignDBRepo extends LocalRepo { function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) { throw new MWException( get_class($this) . ': write operations are not supported' ); } - function deleteBatch( $fileMap ) { + function deleteBatch( $sourceDestPairs ) { throw new MWException( get_class($this) . ': write operations are not supported' ); } } diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php index 80325752..4c530b51 100644 --- a/includes/filerepo/ForeignDBViaLBRepo.php +++ b/includes/filerepo/ForeignDBViaLBRepo.php @@ -1,7 +1,14 @@ $time ) ); if ( !$img ) { @@ -30,7 +36,7 @@ class Image extends LocalFile { * 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 + * @param $name String: name of the image, used to create a title object using Title::makeTitleSafe * @return image object or null if invalid title * @deprecated */ @@ -55,8 +61,8 @@ class Image extends LocalFile { * 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? + * @param $name String: name of the image, without the leading "Image:" + * @param $fromSharedDirectory Boolean: Should this be in $wgSharedUploadPath? * @return string URL of $name image * @deprecated */ diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php index b6b4bfed..5489ecb2 100644 --- a/includes/filerepo/LocalFile.php +++ b/includes/filerepo/LocalFile.php @@ -1,5 +1,9 @@ img_name ); $file = new self( $title, $repo ); $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 ) { + + if ( $timestamp ) { $conds['img_timestamp'] = $timestamp; } - $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), $conds, $fname ); - if( $row ) { + + $dbr = $repo->getSlaveDB(); + $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ ); + + if ( $row ) { return self::newFromRow( $row, $repo ); } else { return false; } } - + /** * Fields in the image table */ @@ -121,10 +128,12 @@ class LocalFile extends File { * Do not call this except from inside a repo class. */ function __construct( $title, $repo ) { - if( !is_object( $title ) ) { + if ( !is_object( $title ) ) { throw new MWException( __CLASS__ . ' constructor given bogus title.' ); } + parent::__construct( $title, $repo ); + $this->metadata = ''; $this->historyLine = 0; $this->historyRes = null; @@ -132,11 +141,12 @@ class LocalFile extends File { } /** - * Get the memcached key for the main data for this file, or false if + * Get the memcached key for the main data for this file, or false if * there is no access to the shared cache. */ function getCacheKey() { $hashedName = md5( $this->getName() ); + return $this->repo->getSharedCacheKey( 'file', $hashedName ); } @@ -145,13 +155,16 @@ class LocalFile extends File { */ function loadFromCache() { global $wgMemc; + wfProfileIn( __METHOD__ ); $this->dataLoaded = false; $key = $this->getCacheKey(); + if ( !$key ) { wfProfileOut( __METHOD__ ); return false; } + $cachedValues = $wgMemc->get( $key ); // Check if the key existed and belongs to this version of MediaWiki @@ -163,6 +176,7 @@ class LocalFile extends File { } $this->dataLoaded = true; } + if ( $this->dataLoaded ) { wfIncrStats( 'image_cache_hit' ); } else { @@ -178,14 +192,18 @@ class LocalFile extends File { */ function saveToCache() { global $wgMemc; + $this->load(); $key = $this->getCacheKey(); + if ( !$key ) { return; } + $fields = $this->getCacheFields( '' ); $cache = array( 'version' => MW_FILE_VERSION ); $cache['fileExists'] = $this->fileExists; + if ( $this->fileExists ) { foreach ( $fields as $field ) { $cache[$field] = $this->$field; @@ -206,9 +224,11 @@ class LocalFile extends File { 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 == '' ) { return $fields; } + if ( !isset( $results[$prefix] ) ) { $prefixedFields = array(); foreach ( $fields as $field ) { @@ -216,6 +236,7 @@ class LocalFile extends File { } $results[$prefix] = $prefixedFields; } + return $results[$prefix]; } @@ -234,6 +255,7 @@ class LocalFile extends File { $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ), array( 'img_name' => $this->getName() ), $fname ); + if ( $row ) { $this->loadFromRow( $row ); } else { @@ -250,15 +272,20 @@ class LocalFile extends File { function decodeRow( $row, $prefix = 'img_' ) { $array = (array)$row; $prefixLength = strlen( $prefix ); + // Sanity check prefix once if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) { throw new MWException( __METHOD__ . ': incorrect $prefix parameter' ); } + $decoded = array(); + foreach ( $array as $name => $value ) { $decoded[substr( $name, $prefixLength )] = $value; } + $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] ); + if ( empty( $decoded['major_mime'] ) ) { $decoded['mime'] = 'unknown/unknown'; } else { @@ -267,8 +294,10 @@ class LocalFile extends File { } $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime']; } + # Trim zero padding from char/binary field $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" ); + return $decoded; } @@ -278,9 +307,11 @@ class LocalFile extends File { function loadFromRow( $row, $prefix = 'img_' ) { $this->dataLoaded = true; $array = $this->decodeRow( $row, $prefix ); + foreach ( $array as $name => $value ) { $this->$name = $value; } + $this->fileExists = true; $this->maybeUpgradeRow(); } @@ -305,6 +336,7 @@ class LocalFile extends File { if ( wfReadOnly() ) { return; } + if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) { @@ -337,6 +369,7 @@ class LocalFile extends File { wfProfileOut( __METHOD__ ); return; } + $dbw = $this->repo->getMasterDB(); list( $major, $minor ) = self::splitMime( $this->mime ); @@ -359,6 +392,7 @@ class LocalFile extends File { ), array( 'img_name' => $this->getName() ), __METHOD__ ); + $this->saveToCache(); wfProfileOut( __METHOD__ ); } @@ -374,15 +408,18 @@ class LocalFile extends File { $this->dataLoaded = true; $fields = $this->getCacheFields( '' ); $fields[] = 'fileExists'; + foreach ( $fields as $field ) { if ( isset( $info[$field] ) ) { $this->$field = $info[$field]; } } + // Fix up mime fields if ( isset( $info['major_mime'] ) ) { $this->mime = "{$info['major_mime']}/{$info['minor_mime']}"; } elseif ( isset( $info['mime'] ) ) { + $this->mime = $info['mime']; list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime ); } } @@ -396,7 +433,7 @@ class LocalFile extends File { /** isVisible inhereted */ function isMissing() { - if( $this->missing === null ) { + if ( $this->missing === null ) { list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY ); $this->missing = !$fileExists; } @@ -410,6 +447,7 @@ class LocalFile extends File { */ public function getWidth( $page = 1 ) { $this->load(); + if ( $this->isMultipage() ) { $dim = $this->getHandler()->getPageDimensions( $this, $page ); if ( $dim ) { @@ -429,6 +467,7 @@ class LocalFile extends File { */ public function getHeight( $page = 1 ) { $this->load(); + if ( $this->isMultipage() ) { $dim = $this->getHandler()->getPageDimensions( $this, $page ); if ( $dim ) { @@ -448,9 +487,10 @@ class LocalFile extends File { */ function getUser( $type = 'text' ) { $this->load(); - if( $type == 'text' ) { + + if ( $type == 'text' ) { return $this->user_text; - } elseif( $type == 'id' ) { + } elseif ( $type == 'id' ) { return $this->user; } } @@ -521,6 +561,7 @@ class LocalFile extends File { function migrateThumbFile( $thumbName ) { $thumbDir = $this->getThumbPath(); $thumbPath = "$thumbDir/$thumbName"; + if ( is_dir( $thumbPath ) ) { // Directory where file should be // This happened occasionally due to broken migration code in 1.5 @@ -535,6 +576,7 @@ class LocalFile extends File { // Doesn't exist anymore clearstatcache(); } + if ( is_file( $thumbDir ) ) { // File where directory should be unlink( $thumbDir ); @@ -552,6 +594,7 @@ class LocalFile extends File { */ function getThumbnails() { $this->load(); + $files = array(); $dir = $this->getThumbPath(); @@ -560,10 +603,11 @@ class LocalFile extends File { if ( $handle ) { while ( false !== ( $file = readdir( $handle ) ) ) { - if ( $file{0} != '.' ) { + if ( $file { 0 } != '.' ) { $files[] = $file; } } + closedir( $handle ); } } @@ -585,8 +629,10 @@ class LocalFile extends File { */ function purgeHistory() { global $wgMemc; + $hashedName = md5( $this->getName() ); $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName ); + if ( $oldKey ) { $wgMemc->delete( $oldKey ); } @@ -611,10 +657,12 @@ class LocalFile extends File { */ function purgeThumbnails() { global $wgUseSquid; + // Delete thumbnails $files = $this->getThumbnails(); $dir = $this->getThumbPath(); $urls = array(); + foreach ( $files as $file ) { # Check that the base file name is part of the thumb name # This is a basic sanity check to avoid erasing unrelated directories @@ -641,31 +689,42 @@ class LocalFile extends File { $conds = $opts = $join_conds = array(); $eq = $inc ? '=' : ''; $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() ); - if( $start ) { + + if ( $start ) { $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) ); } - if( $end ) { + + if ( $end ) { $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) ); } - if( $limit ) { + + if ( $limit ) { $opts['LIMIT'] = $limit; } + // Search backwards for time > x queries $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC'; $opts['ORDER BY'] = "oi_timestamp $order"; $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' ); - wfRunHooks( 'LocalFile::getHistory', array( &$this, &$tables, &$fields, + 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 ); + + foreach ( $res as $row ) { + if ( $this->repo->oldFileFromRowFactory ) { + $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo ); + } else { + $r[] = OldLocalFile::newFromRow( $row, $this->repo ); + } } - if( $order == 'ASC' ) { + + if ( $order == 'ASC' ) { $r = array_reverse( $r ); // make sure it ends up descending } + return $r; } @@ -694,13 +753,12 @@ class LocalFile extends File { array( 'img_name' => $this->title->getDBkey() ), $fname ); + if ( 0 == $dbr->numRows( $this->historyRes ) ) { - $dbr->freeResult( $this->historyRes ); $this->historyRes = null; return false; } } elseif ( $this->historyLine == 1 ) { - $dbr->freeResult( $this->historyRes ); $this->historyRes = $dbr->select( 'oldimage', '*', array( 'oi_name' => $this->title->getDBkey() ), $fname, @@ -717,8 +775,8 @@ class LocalFile extends File { */ public function resetHistory() { $this->historyLine = 0; + if ( !is_null( $this->historyRes ) ) { - $this->repo->getSlaveDB()->freeResult( $this->historyRes ); $this->historyRes = null; } } @@ -739,14 +797,16 @@ class LocalFile extends File { /** * Upload a file and record it in the DB - * @param string $srcPath Source path or virtual URL - * @param string $comment Upload description - * @param string $pageText Text to use for the new description page, if a new description page is created - * @param integer $flags Flags for publish() - * @param array $props File properties, if known. This can be used to reduce the - * upload time when uploading virtual URLs for which the file info - * is already known - * @param string $timestamp Timestamp for img_timestamp, or false to use the current time + * @param $srcPath String: source path or virtual URL + * @param $comment String: upload description + * @param $pageText String: text to use for the new description page, + * if a new description page is created + * @param $flags Integer: flags for publish() + * @param $props Array: File properties, if known. This can be used to reduce the + * upload time when uploading virtual URLs for which the file info + * is already known + * @param $timestamp String: timestamp for img_timestamp, or false to use the current time + * @param $user Mixed: User object or null to use $wgUser * * @return FileRepoStatus object. On success, the value member contains the * archive name, or an empty string if it was a new file. @@ -754,12 +814,15 @@ class LocalFile extends File { function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) { $this->lock(); $status = $this->publish( $srcPath, $flags ); + if ( $status->ok ) { if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) { $status->fatal( 'filenotfound', $srcPath ); } } + $this->unlock(); + return $status; } @@ -771,9 +834,11 @@ class LocalFile extends File { $watch = false, $timestamp = false ) { $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source ); + if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) { return false; } + if ( $watch ) { global $wgUser; $wgUser->addWatch( $this->getTitle() ); @@ -785,11 +850,12 @@ 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, $user = null ) - { - if( is_null( $user ) ) { + function recordUpload2( + $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null + ) { + if ( is_null( $user ) ) { global $wgUser; - $user = $wgUser; + $user = $wgUser; } $dbw = $this->repo->getMasterDB(); @@ -798,27 +864,30 @@ class LocalFile extends File { if ( !$props ) { $props = $this->repo->getFileProps( $this->getVirtualUrl() ); } + + if ( $timestamp === false ) { + $timestamp = $dbw->timestamp(); + } + $props['description'] = $comment; $props['user'] = $user->getId(); $props['user_text'] = $user->getName(); - $props['timestamp'] = wfTimestamp( TS_MW ); + $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW $this->setProps( $props ); - // Delete thumbnails and refresh the metadata cache + # Delete thumbnails $this->purgeThumbnails(); - $this->saveToCache(); + + # The file is already on its final location, remove it from the squid cache SquidUpdate::purge( array( $this->getURL() ) ); - // Fail now if the file isn't there + # Fail now if the file isn't there if ( !$this->fileExists ) { wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" ); return false; } $reupload = false; - if ( $timestamp === false ) { - $timestamp = $dbw->timestamp(); - } # Test to see if the row exists using INSERT IGNORE # This avoids race conditions by locking the row until the commit, and also @@ -826,7 +895,7 @@ class LocalFile extends File { $dbw->insert( 'image', array( 'img_name' => $this->getName(), - 'img_size'=> $this->size, + 'img_size' => $this->size, 'img_width' => intval( $this->width ), 'img_height' => intval( $this->height ), 'img_bits' => $this->bits, @@ -844,7 +913,7 @@ class LocalFile extends File { 'IGNORE' ); - if( $dbw->affectedRows() == 0 ) { + if ( $dbw->affectedRows() == 0 ) { $reupload = true; # Collision, this is an update of a file @@ -905,13 +974,17 @@ class LocalFile extends File { $action = $reupload ? 'overwrite' : 'upload'; $log->addEntry( $action, $descTitle, $comment, array(), $user ); - if( $descTitle->exists() ) { + if ( $descTitle->exists() ) { # Create a null revision $latest = $descTitle->getLatestRevID(); - $nullRevision = Revision::newNullRevision( $dbw, $descTitle->getArticleId(), - $log->getRcComment(), false ); + $nullRevision = Revision::newNullRevision( + $dbw, + $descTitle->getArticleId(), + $log->getRcComment(), + false + ); $nullRevision->insertOn( $dbw ); - + wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) ); $article->updateRevisionOn( $dbw, $nullRevision ); @@ -919,24 +992,33 @@ class LocalFile extends File { $descTitle->invalidateCache(); $descTitle->purgeSquid(); } else { - // New file; create the description page. - // There's already a log entry, so don't make a second RC entry + # New file; create the description page. + # There's already a log entry, so don't make a second RC entry + # Squid and file cache for the description page are purged by doEdit. $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC ); } - # Hooks, hooks, the magic of hooks... - wfRunHooks( 'FileUpload', array( $this ) ); - # Commit the transaction now, in case something goes wrong later # The most important thing is that files don't get lost, especially archives $dbw->commit(); + # Save to cache and purge the squid + # We shall not saveToCache before the commit since otherwise + # in case of a rollback there is an usable file from memcached + # which in fact doesn't really exist (bug 24978) + $this->saveToCache(); + + # Hooks, hooks, the magic of hooks... + wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) ); + # 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 ) { + + foreach ( $redirs as $redir ) { $update = new HTMLCacheUpdate( $redir, 'imagelinks' ); $update->doUpdate(); } @@ -946,15 +1028,14 @@ class LocalFile extends File { /** * 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. + * destination, move it to an archive. Returns a FileRepoStatus object with + * the archive name in the "value" member on success. * * The archive name should be passed through to recordUpload for database * registration. * - * @param string $sourcePath Local filesystem path to the source image - * @param integer $flags A bitwise combination of: + * @param $srcPath String: local filesystem path to the source image + * @param $flags Integer: a bitwise combination of: * File::DELETE_SOURCE Delete the source file, i.e. move * rather than copy * @return FileRepoStatus object. On success, the value member contains the @@ -962,17 +1043,21 @@ class LocalFile extends File { */ function publish( $srcPath, $flags = 0 ) { $this->lock(); + $dstRel = $this->getRel(); - $archiveName = gmdate( 'YmdHis' ) . '!'. $this->getName(); + $archiveName = gmdate( 'YmdHis' ) . '!' . $this->getName(); $archiveRel = 'archive/' . $this->getHashPath() . $archiveName; $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0; $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags ); + if ( $status->value == 'new' ) { $status->value = ''; } else { $status->value = $archiveName; } + $this->unlock(); + return $status; } @@ -996,12 +1081,14 @@ class LocalFile extends File { 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(); @@ -1014,7 +1101,7 @@ class LocalFile extends File { // Purge the new image $this->purgeEverything(); } - + return $status; } @@ -1032,6 +1119,7 @@ class LocalFile extends File { */ function delete( $reason, $suppress = false ) { $this->lock(); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); $batch->addCurrent(); @@ -1040,7 +1128,7 @@ class LocalFile extends File { $result = $dbw->select( 'oldimage', array( 'oi_archive_name' ), array( 'oi_name' => $this->getName() ) ); - while ( $row = $dbw->fetchObject( $result ) ) { + foreach ( $result as $row ) { $batch->addOld( $row->oi_archive_name ); } $status = $batch->execute(); @@ -1053,6 +1141,7 @@ class LocalFile extends File { } $this->unlock(); + return $status; } @@ -1064,21 +1153,26 @@ class LocalFile extends File { * * Cache purging is done; logging is caller's responsibility. * - * @param $reason - * @param $suppress + * @param $archiveName String + * @param $reason String + * @param $suppress Boolean * @throws MWException or FSException on database or file store failure * @return FileRepoStatus object. */ - function deleteOld( $archiveName, $reason, $suppress=false ) { + function deleteOld( $archiveName, $reason, $suppress = false ) { $this->lock(); + $batch = new LocalFileDeleteBatch( $this, $reason, $suppress ); $batch->addOld( $archiveName ); $status = $batch->execute(); + $this->unlock(); + if ( $status->ok ) { $this->purgeDescription(); $this->purgeHistory(); } + return $status; } @@ -1090,17 +1184,20 @@ class LocalFile extends File { * * @param $versions set of record ids of deleted items to restore, * or empty to restore all revisions. - * @param $unuppress + * @param $unsuppress Boolean * @return FileRepoStatus */ function restore( $versions = array(), $unsuppress = false ) { $batch = new LocalFileRestoreBatch( $this, $unsuppress ); + if ( !$versions ) { $batch->addAll(); } else { $batch->addIds( $versions ); } + $status = $batch->execute(); + if ( !$status->ok ) { return $status; } @@ -1109,6 +1206,7 @@ class LocalFile extends File { $cleanupStatus->successCount = 0; $cleanupStatus->failCount = 0; $status->merge( $cleanupStatus ); + return $status; } @@ -1174,10 +1272,12 @@ class LocalFile extends File { */ function lock() { $dbw = $this->repo->getMasterDB(); + if ( !$this->locked ) { $dbw->begin(); $this->locked++; } + return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ ); } @@ -1205,7 +1305,7 @@ class LocalFile extends File { } } // LocalFile class -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ /** * Helper class for file deletion @@ -1240,25 +1340,33 @@ class LocalFileDeleteBatch { unset( $oldRels['.'] ); $deleteCurrent = true; } + return array( $oldRels, $deleteCurrent ); } /*protected*/ function getHashes() { $hashes = array(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + if ( $deleteCurrent ) { $hashes['.'] = $this->file->getSha1(); } + if ( count( $oldRels ) ) { $dbw = $this->file->repo->getMasterDB(); - $res = $dbw->select( 'oldimage', array( 'oi_archive_name', 'oi_sha1' ), - 'oi_archive_name IN(' . $dbw->makeList( array_keys( $oldRels ) ) . ')', - __METHOD__ ); - while ( $row = $dbw->fetchObject( $res ) ) { + $res = $dbw->select( + 'oldimage', + array( 'oi_archive_name', 'oi_sha1' ), + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')', + __METHOD__ + ); + + foreach ( $res as $row ) { if ( rtrim( $row->oi_sha1, "\0" ) === '' ) { // Get the hash from the file $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name ); $props = $this->file->repo->getFileProps( $oldUrl ); + if ( $props['fileExists'] ) { // Upgrade the oldimage row $dbw->update( 'oldimage', @@ -1274,10 +1382,13 @@ class LocalFileDeleteBatch { } } } + $missing = array_diff_key( $this->srcRels, $hashes ); + foreach ( $missing as $name => $rel ) { $this->status->error( 'filedelete-old-unregistered', $name ); } + foreach ( $hashes as $name => $hash ) { if ( !$hash ) { $this->status->error( 'filedelete-missing', $this->srcRels[$name] ); @@ -1290,6 +1401,7 @@ class LocalFileDeleteBatch { function doDBInserts() { global $wgUser; + $dbw = $this->file->repo->getMasterDB(); $encTimestamp = $dbw->addQuotes( $dbw->timestamp() ); $encUserId = $dbw->addQuotes( $wgUser->getId() ); @@ -1377,6 +1489,7 @@ class LocalFileDeleteBatch { function doDBDeletes() { $dbw = $this->file->repo->getMasterDB(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); + if ( count( $oldRels ) ) { $dbw->delete( 'oldimage', array( @@ -1384,6 +1497,7 @@ class LocalFileDeleteBatch { 'oi_archive_name' => array_keys( $oldRels ) ), __METHOD__ ); } + if ( $deleteCurrent ) { $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ ); } @@ -1401,14 +1515,16 @@ class LocalFileDeleteBatch { $privateFiles = array(); list( $oldRels, $deleteCurrent ) = $this->getOldRels(); $dbw = $this->file->repo->getMasterDB(); - if( !empty( $oldRels ) ) { + + 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) ) . ')', - $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ), + 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')', + $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ), __METHOD__ ); - while( $row = $dbw->fetchObject( $res ) ) { + + foreach ( $res as $row ) { $privateFiles[$row->oi_archive_name] = 1; } } @@ -1417,6 +1533,7 @@ class LocalFileDeleteBatch { $this->deletionBatch = array(); $ext = $this->file->getExtension(); $dotExt = $ext === '' ? '' : ".$ext"; + foreach ( $this->srcRels as $name => $srcRel ) { // Skip files that have no hash (missing source). // Keep private files where they are. @@ -1441,6 +1558,7 @@ class LocalFileDeleteBatch { // Execute the file deletion batch $status = $this->file->repo->deleteBatch( $this->deletionBatch ); + if ( !$status->isGood() ) { $this->status->merge( $status ); } @@ -1457,6 +1575,7 @@ class LocalFileDeleteBatch { // Purge squid if ( $wgUseSquid ) { $urls = array(); + foreach ( $this->srcRels as $srcRel ) { $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) ); $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel; @@ -1470,6 +1589,7 @@ class LocalFileDeleteBatch { // Commit and return $this->file->unlock(); wfProfileOut( __METHOD__ ); + return $this->status; } @@ -1478,19 +1598,25 @@ class LocalFileDeleteBatch { */ function removeNonexistentFiles( $batch ) { $files = $newBatch = array(); - foreach( $batch as $batchItem ) { + + foreach ( $batch as $batchItem ) { list( $src, $dest ) = $batchItem; $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src ); } + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); - foreach( $batch as $batchItem ) - if( $result[$batchItem[0]] ) + + foreach ( $batch as $batchItem ) { + if ( $result[$batchItem[0]] ) { $newBatch[] = $batchItem; + } + } + return $newBatch; } } -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ /** * Helper class for file undeletion @@ -1536,6 +1662,7 @@ class LocalFileRestoreBatch { */ function execute() { global $wgLang; + if ( !$this->all && !$this->ids ) { // Do nothing return $this->file->repo->newGood(); @@ -1548,7 +1675,8 @@ class LocalFileRestoreBatch { // Fetch all or selected archived revisions for the file, // sorted from the most recent to the oldest. $conditions = array( 'fa_name' => $this->file->getName() ); - if( !$this->all ) { + + if ( !$this->all ) { $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')'; } @@ -1565,7 +1693,8 @@ class LocalFileRestoreBatch { $deleteIds = array(); $first = true; $archiveNames = array(); - while( $row = $dbw->fetchObject( $result ) ) { + + foreach ( $result as $row ) { $idsPresent[] = $row->fa_id; if ( $row->fa_name != $this->file->getName() ) { @@ -1573,6 +1702,7 @@ class LocalFileRestoreBatch { $status->failCount++; continue; } + if ( $row->fa_storage_key == '' ) { // Revision was missing pre-deletion $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) ); @@ -1584,12 +1714,13 @@ class LocalFileRestoreBatch { $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) ); + # Fix leading zero if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) { $sha1 = substr( $sha1, 1 ); } - if( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown' + if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown' || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown' || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN' || is_null( $row->fa_metadata ) ) { @@ -1624,23 +1755,27 @@ class LocalFileRestoreBatch { 'img_timestamp' => $row->fa_timestamp, 'img_sha1' => $sha1 ); + // The live (current) version cannot be hidden! - if( !$this->unsuppress && $row->fa_deleted ) { + if ( !$this->unsuppress && $row->fa_deleted ) { $storeBatch[] = array( $deletedUrl, 'public', $destRel ); $this->cleanupBatch[] = $row->fa_storage_key; } } else { $archiveName = $row->fa_archive_name; - if( $archiveName == '' ) { + + if ( $archiveName == '' ) { // This was originally a current version; we // have to devise a new archive name for it. // Format is ! $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp ); + do { $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name; $timestamp++; } while ( isset( $archiveNames[$archiveName] ) ); } + $archiveNames[$archiveName] = true; $destRel = $this->file->getArchiveRel( $archiveName ); $insertBatch[] = array( @@ -1663,19 +1798,23 @@ class LocalFileRestoreBatch { } $deleteIds[] = $row->fa_id; - if( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) { + + if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) { // private files can stay where they are $status->successCount++; } else { $storeBatch[] = array( $deletedUrl, 'public', $destRel ); $this->cleanupBatch[] = $row->fa_storage_key; } + $first = false; } + unset( $result ); // Add a warning to the status object for missing IDs $missingIds = array_diff( $this->ids, $idsPresent ); + foreach ( $missingIds as $id ) { $status->error( 'undelete-missing-filearchive', $id ); } @@ -1692,6 +1831,7 @@ class LocalFileRestoreBatch { // Store batch returned a critical error -- this usually means nothing was stored // Stop now and return an error $this->file->unlock(); + return $status; } @@ -1704,9 +1844,11 @@ class LocalFileRestoreBatch { if ( $insertCurrent ) { $dbw->insert( 'image', $insertCurrent, __METHOD__ ); } + if ( $insertBatch ) { $dbw->insert( 'oldimage', $insertBatch, __METHOD__ ); } + if ( $deleteIds ) { $dbw->delete( 'filearchive', array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ), @@ -1714,8 +1856,8 @@ class LocalFileRestoreBatch { } // If store batch is empty (all files are missing), deletion is to be considered successful - if( $status->successCount > 0 || !$storeBatch ) { - if( !$exists ) { + if ( $status->successCount > 0 || !$storeBatch ) { + if ( !$exists ) { wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" ); // Update site_stats @@ -1729,7 +1871,9 @@ class LocalFileRestoreBatch { $this->file->purgeHistory(); } } + $this->file->unlock(); + return $status; } @@ -1738,12 +1882,17 @@ class LocalFileRestoreBatch { */ function removeNonexistentFiles( $triplets ) { $files = $filteredTriplets = array(); - foreach( $triplets as $file ) + foreach ( $triplets as $file ) $files[$file[0]] = $file[0]; + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); - foreach( $triplets as $file ) - if( $result[$file[0]] ) + + foreach ( $triplets as $file ) { + if ( $result[$file[0]] ) { $filteredTriplets[] = $file; + } + } + return $filteredTriplets; } @@ -1753,15 +1902,20 @@ class LocalFileRestoreBatch { function removeNonexistentFromCleanup( $batch ) { $files = $newBatch = array(); $repo = $this->file->repo; - foreach( $batch as $file ) { + + foreach ( $batch as $file ) { $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' . rawurlencode( $repo->getDeletedHashPath( $file ) . $file ); } $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); - foreach( $batch as $file ) - if( $result[$file] ) + + foreach ( $batch as $file ) { + if ( $result[$file] ) { $newBatch[] = $file; + } + } + return $newBatch; } @@ -1773,13 +1927,16 @@ class LocalFileRestoreBatch { if ( !$this->cleanupBatch ) { return $this->file->repo->newGood(); } + $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch ); + $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch ); + return $status; } } -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ /** * Helper class for file movement @@ -1820,29 +1977,35 @@ class LocalFileMoveBatch { array( 'oi_name' => $this->oldName ), __METHOD__ ); - while( $row = $this->db->fetchObject( $result ) ) { + + foreach ( $result as $row ) { $oldName = $row->oi_archive_name; $bits = explode( '!', $oldName, 2 ); - if( count( $bits ) != 2 ) { + + if ( count( $bits ) != 2 ) { wfDebug( "Invalid old file name: $oldName \n" ); continue; } + list( $timestamp, $filename ) = $bits; - if( $this->oldName != $filename ) { + + if ( $this->oldName != $filename ) { wfDebug( "Invalid old file name: $oldName \n" ); continue; } + $this->oldCount++; + // Do we want to add those to oldCount? - if( $row->oi_deleted & File::DELETED_FILE ) { + 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 ); } /** @@ -1858,19 +2021,23 @@ class LocalFileMoveBatch { 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() ) { + + 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. + * Do the database updates and return a new FileRepoStatus indicating how + * many rows where updated. + * + * @return FileRepoStatus */ function doDBUpdates() { $repo = $this->file->repo; @@ -1878,13 +2045,14 @@ class LocalFileMoveBatch { $dbw = $this->db; // Update current image - $dbw->update( + $dbw->update( 'image', array( 'img_name' => $this->newName ), array( 'img_name' => $this->oldName ), __METHOD__ ); - if( $dbw->affectedRows() ) { + + if ( $dbw->affectedRows() ) { $status->successCount++; } else { $status->failCount++; @@ -1895,11 +2063,12 @@ class LocalFileMoveBatch { 'oldimage', array( 'oi_name' => $this->newName, - 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes($this->oldName), $dbw->addQuotes($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; @@ -1910,34 +2079,42 @@ class LocalFileMoveBatch { /** * 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 ) { + + 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; } /** * Removes non-existent files from move batch. - */ + */ function removeNonexistentFiles( $triplets ) { $files = array(); - foreach( $triplets as $file ) + + foreach ( $triplets as $file ) { $files[$file[0]] = $file[0]; + } + $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY ); $filteredTriplets = array(); - foreach( $triplets as $file ) - if( $result[$file[0]] ) { + + foreach ( $triplets as $file ) { + if ( $result[$file[0]] ) { $filteredTriplets[] = $file; } else { wfDebugLog( 'imagemove', "File {$file[0]} does not exist" ); } + } + return $filteredTriplets; } } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 6c4d21a2..02883c53 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -1,4 +1,12 @@ fetchObject() ) + foreach ( $res as $row ) { $result[] = $this->newFileFromRow( $row ); + } $res->free(); + return $result; } @@ -189,8 +201,8 @@ class LocalRepo extends FSRepo { /** * Invalidates image redirect cache related to that image * - * @param Title $title Title of image - */ + * @param $title Title of page + */ function invalidateImageRedirect( $title ) { global $wgMemc; $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) ); diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php index 2bc61bde..d5a1ee03 100644 --- a/includes/filerepo/NullRepo.php +++ b/includes/filerepo/NullRepo.php @@ -1,4 +1,10 @@ $sha1 ); if( $timestamp ) { $conds['oi_timestamp'] = $timestamp; } - $row = $dbr->selectRow( 'oldimage', $this->getCacheFields( 'oi_' ), $conds, $fname ); + $dbr = $repo->getSlaveDB(); + $row = $dbr->selectRow( 'oldimage', self::selectFields(), $conds, __METHOD__ ); if( $row ) { return self::newFromRow( $row, $repo ); } else { @@ -70,10 +74,10 @@ class OldLocalFile extends LocalFile { } /** - * @param Title $title - * @param FileRepo $repo - * @param string $time Timestamp or null to load by archive name - * @param string $archiveName Archive name or null to load by timestamp + * @param $title Title + * @param $repo FileRepo + * @param $time String: timestamp or null to load by archive name + * @param $archiveName String: archive name or null to load by timestamp */ function __construct( $title, $repo, $time, $archiveName ) { parent::__construct( $title, $repo ); @@ -135,7 +139,7 @@ class OldLocalFile extends LocalFile { } function getUrlRel() { - return 'archive/' . $this->getHashPath() . urlencode( $this->getArchiveName() ); + return 'archive/' . $this->getHashPath() . rawurlencode( $this->getArchiveName() ); } function upgradeRow() { @@ -172,8 +176,8 @@ class OldLocalFile extends LocalFile { } /** - * int $field one of DELETED_* bitfield constants - * for file or revision rows + * @param $field Integer: one of DELETED_* bitfield constants + * for file or revision rows * @return bool */ function isDeleted( $field ) { @@ -193,7 +197,8 @@ class OldLocalFile extends LocalFile { /** * Determine if the current user is allowed to view a particular * field of this image file, if it's marked as deleted. - * @param int $field + * + * @param $field Integer * @return bool */ function userCan( $field ) { diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php index 1465400c..b9996941 100644 --- a/includes/filerepo/RepoGroup.php +++ b/includes/filerepo/RepoGroup.php @@ -1,14 +1,19 @@ getNamespace() != NS_MEDIA && $title->getNamespace() != NS_FILE ) { + throw new MWException( __METHOD__ . ' recieved an Title object with incorrect namespace' ); + } + # Check the cache if ( empty( $options['ignoreRedirect'] ) && empty( $options['private'] ) - && empty( $options['bypassCache'] ) ) + && empty( $options['bypassCache'] ) + && $title->getNamespace() == NS_FILE ) { $useCache = true; $time = isset( $options['time'] ) ? $options['time'] : ''; @@ -224,7 +237,7 @@ class RepoGroup { if ( !$this->reposInitialised ) { $this->initialiseRepos(); } - foreach ( $this->foreignRepos as $key => $repo ) { + foreach ( $this->foreignRepos as $repo ) { if ( $repo->name == $name) return $repo; } @@ -243,8 +256,8 @@ class RepoGroup { * Call a function for each foreign repo, with the repo object as the * first parameter. * - * @param $callback callback The function to call - * @param $params array Optional additional parameters to pass to the function + * @param $callback Callback: the function to call + * @param $params Array: optional additional parameters to pass to the function */ function forEachForeignRepo( $callback, $params = array() ) { foreach( $this->foreignRepos as $repo ) { @@ -258,7 +271,7 @@ class RepoGroup { /** * Does the installation have any foreign repos set up? - * @return bool + * @return Boolean */ function hasForeignRepos() { return (bool)$this->foreignRepos; diff --git a/includes/filerepo/UnregisteredLocalFile.php b/includes/filerepo/UnregisteredLocalFile.php index 6f63cb0b..990a218c 100644 --- a/includes/filerepo/UnregisteredLocalFile.php +++ b/includes/filerepo/UnregisteredLocalFile.php @@ -1,4 +1,10 @@ repo ) { - return $this->repo->getZoneUrl( 'public' ) . '/' . $this->repo->getHashPath( $this->name ) . urlencode( $this->name ); + return $this->repo->getZoneUrl( 'public' ) . '/' . $this->repo->getHashPath( $this->name ) . rawurlencode( $this->name ); } else { return false; } -- cgit v1.2.3-54-g00ecf