summaryrefslogtreecommitdiff
path: root/includes/filerepo
diff options
context:
space:
mode:
Diffstat (limited to 'includes/filerepo')
-rw-r--r--includes/filerepo/ArchivedFile.php72
-rw-r--r--includes/filerepo/FSRepo.php44
-rw-r--r--includes/filerepo/File.php221
-rw-r--r--includes/filerepo/FileRepo.php174
-rw-r--r--includes/filerepo/FileRepoStatus.php151
-rw-r--r--includes/filerepo/ForeignAPIFile.php101
-rw-r--r--includes/filerepo/ForeignAPIRepo.php110
-rw-r--r--includes/filerepo/ForeignDBFile.php33
-rw-r--r--includes/filerepo/ForeignDBRepo.php13
-rw-r--r--includes/filerepo/ForeignDBViaLBRepo.php39
-rw-r--r--includes/filerepo/Image.php74
-rw-r--r--includes/filerepo/LocalFile.php533
-rw-r--r--includes/filerepo/LocalRepo.php103
-rw-r--r--includes/filerepo/NullRepo.php8
-rw-r--r--includes/filerepo/OldLocalFile.php192
-rw-r--r--includes/filerepo/README14
-rw-r--r--includes/filerepo/RepoGroup.php74
-rw-r--r--includes/filerepo/UnregisteredLocalFile.php9
18 files changed, 1327 insertions, 638 deletions
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 @@
<?php
/**
- * @addtogroup Media
+ * @ingroup Media
*/
class ArchivedFile
{
@@ -26,9 +26,9 @@ class ArchivedFile
$timestamp, # time of upload
$dataLoaded, # Whether or not all this has been loaded from the database (loadFromXxx)
$deleted; # Bitfield akin to rev_deleted
-
- /**#@-*/
-
+
+ /**#@-*/
+
function ArchivedFile( $title, $id=0, $key='' ) {
if( !is_object($title) ) {
throw new MWException( 'ArchivedFile constructor given bogus title.' );
@@ -84,19 +84,19 @@ class ArchivedFile
'fa_user_text',
'fa_timestamp',
'fa_deleted' ),
- array(
+ array(
'fa_name' => $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 @@
<?php
/**
- * Implements some public methods and some protected utility functions which
- * are required by multiple child classes. Contains stub functionality for
+ * Implements some public methods and some protected utility functions which
+ * are required by multiple child classes. Contains stub functionality for
* unimplemented public methods.
*
- * Stub functions which should be overridden are marked with STUB. Some more
+ * Stub functions which should be overridden are marked with STUB. Some more
* concrete functions are also typically overridden by child classes.
*
* 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);
@@ -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 ) ) {
@@ -90,6 +91,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
* STUB
@@ -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;
}
/**
@@ -902,6 +948,22 @@ abstract class File {
}
/**
+ * 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.
*
* Moves the files into an archive directory (or deletes them)
@@ -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 @@
<?php
/**
- * Generic operation result class
- * Has warning/error list, boolean status and arbitrary value
+ * Generic operation result class for FileRepo-related operations
+ * @ingroup FileRepo
*/
-class FileRepoStatus {
- var $ok = true;
- var $value;
-
- /** Counters for batch operations */
- var $successCount = 0, $failCount = 0;
-
- /*semi-private*/ var $errors = array();
- /*semi-private*/ var $cleanCallback = false;
-
+class FileRepoStatus extends Status {
/**
* Factory function for fatal errors
*/
- static function newFatal( $repo, $message /*, parameters...*/ ) {
+ static function newFatal( $repo /*, parameters...*/ ) {
$params = array_slice( func_get_args(), 1 );
$result = new self( $repo );
call_user_func_array( array( &$result, 'error' ), $params );
@@ -30,142 +21,10 @@ class FileRepoStatus {
$result->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 .= "</{$this->type}>\n";
- return $xml;
- }
-
- /**
- * Get the error list as XML
- */
- function getXML() {
- $xml = "<errors>\n";
- foreach ( $this->errors as $error ) {
- $xml .= $this->getItemXML( $error );
- }
- $xml .= "</errors>\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 @@
+<?php
+
+/**
+ * Very hacky and inefficient
+ * do not use :D
+ *
+ * @ingroup FileRepo
+ */
+class ForeignAPIFile extends File {
+ function __construct( $title, $repo, $info ) {
+ parent::__construct( $title, $repo );
+ $this->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 @@
+<?php
+
+/**
+ * A foreign repository with a remote MediaWiki with an API thingy
+ * Very hacky and inefficient
+ * do not use except for testing :D
+ *
+ * Example config:
+ *
+ * $wgForeignFileRepos[] = array(
+ * 'class' => '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 @@
<?php
+/**
+ * @ingroup FileRepo
+ */
class ForeignDBFile extends LocalFile {
- static function newFromTitle( $title, $repo ) {
+ static function newFromTitle( $title, $repo, $unused = null ) {
return new self( $title, $repo );
}
+ /**
+ * Create a ForeignDBFile from a title
+ * Do not call this except from inside a repo class.
+ */
+ static function newFromRow( $row, $repo ) {
+ $title = Title::makeTitle( NS_IMAGE, $row->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 @@
+<?php
+
+/**
+ * A foreign repository with a MediaWiki database accessible via the configured LBFactory
+ * @ingroup FileRepo
+ */
+class ForeignDBViaLBRepo extends LocalRepo {
+ var $wiki, $dbName, $tablePrefix;
+ var $fileFactory = array( 'ForeignDBFile', 'newFromTitle' );
+ var $fileFromRowFactory = array( 'ForeignDBFile', 'newFromRow' );
+
+ function __construct( $info ) {
+ parent::__construct( $info );
+ $this->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 @@
+<?php
+
+/**
+ * Backwards compatibility class
+ * @deprecated
+ * @ingroup FileRepo
+ */
+class Image extends LocalFile {
+ function __construct( $title ) {
+ wfDeprecated( __METHOD__ );
+ $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 ) {
+ 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() {
@@ -1085,84 +1182,17 @@ 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 @@
<?php
+/**
+ * @defgroup FileRepo FileRepo
+ *
+ * @file
+ * @ingroup FileRepo
+ */
/**
+ * @ingroup FileRepo
* Prioritized list of file repositories
- * @addtogroup filerepo
*/
class RepoGroup {
var $localRepo, $foreignRepos, $reposInitialised = false;
@@ -39,9 +45,9 @@ class RepoGroup {
}
/**
- * Construct a group of file repositories.
- * @param array $data Array of repository info arrays.
- * Each info array is an associative array with the 'class' member
+ * Construct a group of file repositories.
+ * @param array $data Array of repository info arrays.
+ * Each info array is an associative array with the 'class' member
* giving the class name. The entire array is passed to the repository
* constructor as the first parameter.
*/
@@ -54,27 +60,52 @@ class RepoGroup {
* Search repositories for an image.
* You can also use wfGetFile() to do this.
* @param mixed $title Title object or string
- * @param mixed $time The 14-char timestamp the file should have
+ * @param mixed $time The 14-char timestamp the file should have
* been uploaded, or false for the current version
+ * @param mixed $flags FileRepo::FIND_ flags
* @return File object or false if it is not found
*/
- function findFile( $title, $time = false ) {
+ function findFile( $title, $time = false, $flags = 0 ) {
if ( !$this->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 @@
<?php
/**
- * A file object referring to either a standalone local file, or a file in a
+ * A file object referring to either a standalone local file, or a file in a
* local repository with no database, for example an FSRepo repository.
*
* Read-only.
*
- * TODO: Currently it doesn't really work in the repository role, there are
- * lots of functions missing. It is used by the WebStore extension in the
+ * TODO: Currently it doesn't really work in the repository role, there are
+ * lots of functions missing. It is used by the WebStore extension in the
* standalone role.
+ *
+ * @ingroup FileRepo
*/
class UnregisteredLocalFile extends File {
var $title, $path, $mime, $handler, $dims;
@@ -106,4 +108,3 @@ class UnregisteredLocalFile extends File {
}
}
}
-