hide their (unrelated) log entries.
* Added $wgOpenSearchDefaultLimit defining the default number of entries to show
on action=opensearch API call.
+* For namespaces with $wgNamespaceProtection (including the MediaWiki
+ namespace), the "protect" tab will be shown only if there are restriction
+ levels available that would restrict editing beyond what
+ $wgNamespaceProtection already applies. The protection form will offer only
+ those protection levels.
=== Bug fixes in 1.23 ===
* (bug 41759) The "updated since last visit" markers (on history pages, recent
wfLogProfilingData();
function wfImageAuthMain() {
- global $wgImgAuthPublicTest, $wgRequest;
+ global $wgImgAuthPublicTest, $wgImgAuthUrlPathMap, $wgRequest;
// See if this is a public Wiki (no protections).
if ( $wgImgAuthPublicTest
// Check for bug 28235: QUERY_STRING overriding the correct extension
$whitelist = array();
- $dotPos = strrpos( $path, '.' );
- if ( $dotPos !== false ) {
- $whitelist[] = substr( $path, $dotPos + 1 );
+ $extension = FileBackend::extensionFromPath( $path );
+ if ( $extension != '' ) {
+ $whitelist[] = $extension;
}
if ( !$wgRequest->checkUrlExtension( $whitelist ) ) {
return;
}
+ // Various extensions may have their own backends that need access.
+ // Check if there is a special backend and storage base path for this file.
+ foreach ( $wgImgAuthUrlPathMap as $prefix => $storageDir ) {
+ $prefix = rtrim( $prefix, '/' ) . '/'; // implicit trailing slash
+ if ( strpos( $path, $prefix ) === 0 ) {
+ $be = FileBackendGroup::singleton()->backendFromPath( $storageDir );
+ $filename = $storageDir . substr( $path, strlen( $prefix ) ); // strip prefix
+ if ( $be->fileExists( array( 'src' => $filename ) ) ) {
+ wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." );
+ $be->streamFile( array( 'src' => $filename ),
+ array( 'Cache-Control: private', 'Vary: Cookie' ) );
+ } else {
+ wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $filename );
+ }
+ return;
+ }
+ }
+
// Get the local file repository
$repo = RepoGroup::singleton()->getRepo( 'local' );
$args = func_get_args();
array_shift( $args );
array_shift( $args );
+ $args = ( isset( $args[0] ) && is_array( $args[0] ) ) ? $args[0] : $args;
$msgHdr = wfMessage( $msg1 )->escaped();
$detailMsgKey = $wgImgAuthDetails ? $msg2 : 'badaccess-group0';
*/
$wgImgAuthPublicTest = true;
+/**
+ * Map of relative URL directories to match to internal mwstore:// base storage paths.
+ * For img_auth.php requests, everything after "img_auth.php/" is checked to see
+ * if starts with any of the prefixes defined here. The prefixes should not overlap.
+ * The prefix that matches has a corresponding storage path, which the rest of the URL
+ * is assumed to be relative to. The file at that path (or a 404) is send to the client.
+ *
+ * Example:
+ * $wgImgAuthUrlPathMap['/timeline/'] = 'mwstore://local-fs/timeline-render/';
+ * The above maps ".../img_auth.php/timeline/X" to "mwstore://local-fs/timeline-render/".
+ * The name "local-fs" should correspond by name to an entry in $wgFileBackends.
+ *
+ * @see $wgFileBackends
+ */
+$wgImgAuthUrlPathMap = array();
+
/**
* File repository structures
*
}
}
- if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
+ if ( $this->mTitle->isProtected( 'edit' ) &&
+ MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' )
+ ) {
# Is the title semi-protected?
if ( $this->mTitle->isSemiProtected() ) {
$noticeMsg = 'semiprotectedpagewarning';
$attribs = array( 'style' => 'display:none;' );
} else {
$classes = array(); // Textarea CSS
- if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
+ if ( $this->mTitle->isProtected( 'edit' ) &&
+ MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== array( '' )
+ ) {
# Is the title semi-protected?
if ( $this->mTitle->isSemiProtected() ) {
$classes[] = 'mw-textarea-sprotected';
? $wgNamespaceContentModels[$index]
: null;
}
+
+ /**
+ * Determine which restriction levels it makes sense to use in a namespace,
+ * optionally filtered by a user's rights.
+ *
+ * @since 1.23
+ * @param int $index Index to check
+ * @param User $user User to check
+ * @return array
+ */
+ public static function getRestrictionLevels( $index, User $user = null ) {
+ global $wgNamespaceProtection, $wgRestrictionLevels;
+
+ if ( !isset( $wgNamespaceProtection[$index] ) ) {
+ // All levels are valid if there's no namespace restriction.
+ // But still filter by user, if necessary
+ $levels = $wgRestrictionLevels;
+ if ( $user ) {
+ $levels = array_values( array_filter( $levels, function ( $level ) use ( $user ) {
+ $right = $level;
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+ return ( $right == '' || $user->isAllowed( $right ) );
+ } ) );
+ }
+ return $levels;
+ }
+
+ // First, get the list of groups that can edit this namespace.
+ $namespaceGroups = array();
+ $combine = 'array_merge';
+ foreach ( (array)$wgNamespaceProtection[$index] as $right ) {
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+ if ( $right != '' ) {
+ $namespaceGroups = call_user_func( $combine, $namespaceGroups,
+ User::getGroupsWithPermission( $right ) );
+ $combine = 'array_intersect';
+ }
+ }
+
+ // Now, keep only those restriction levels where there is at least one
+ // group that can edit the namespace but would be blocked by the
+ // restriction.
+ $usableLevels = array( '' );
+ foreach ( $wgRestrictionLevels as $level ) {
+ $right = $level;
+ if ( $right == 'sysop' ) {
+ $right = 'editprotected'; // BC
+ }
+ if ( $right == 'autoconfirmed' ) {
+ $right = 'editsemiprotected'; // BC
+ }
+ if ( $right != '' && ( !$user || $user->isAllowed( $right ) ) &&
+ array_diff( $namespaceGroups, User::getGroupsWithPermission( $right ) )
+ ) {
+ $usableLevels[] = $level;
+ }
+ }
+
+ return $usableLevels;
+ }
}
*/
function loadData() {
global $wgRequest, $wgUser;
- global $wgRestrictionLevels;
+ $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(), $wgUser );
$this->mCascade = $this->mTitle->areRestrictionsCascading();
$this->mReason = $wgRequest->getText( 'mwProtect-reason' );
}
$val = $wgRequest->getVal( "mwProtect-level-$action" );
- if ( isset( $val ) && in_array( $val, $wgRestrictionLevels ) ) {
- // Prevent users from setting levels that they cannot later unset
- if ( $val == 'sysop' ) {
- // Special case, rewrite sysop to editprotected
- if ( !$wgUser->isAllowed( 'editprotected' ) ) {
- continue;
- }
- } elseif ( $val == 'autoconfirmed' ) {
- // Special case, rewrite autoconfirmed to editsemiprotected
- if ( !$wgUser->isAllowed( 'editsemiprotected' ) ) {
- continue;
- }
- } elseif ( !$wgUser->isAllowed( $val ) ) {
- continue;
- }
+ if ( isset( $val ) && in_array( $val, $levels ) ) {
$this->mRestrictions[$action] = $val;
}
}
function execute() {
global $wgRequest, $wgOut;
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if ( MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) === array( '' ) ) {
throw new ErrorPageError( 'protect-badnamespace-title', 'protect-badnamespace-text' );
}
* @return String: HTML fragment
*/
function buildSelector( $action, $selected ) {
- global $wgRestrictionLevels, $wgUser;
-
- $levels = array();
- foreach ( $wgRestrictionLevels as $key ) {
- //don't let them choose levels above their own (aka so they can still unprotect and edit the page). but only when the form isn't disabled
- if ( $key == 'sysop' ) {
- //special case, rewrite sysop to editprotected
- if ( !$wgUser->isAllowed( 'editprotected' ) && !$this->disabled ) {
- continue;
- }
- } elseif ( $key == 'autoconfirmed' ) {
- //special case, rewrite autoconfirmed to editsemiprotected
- if ( !$wgUser->isAllowed( 'editsemiprotected' ) && !$this->disabled ) {
- continue;
- }
- } else {
- if ( !$wgUser->isAllowed( $key ) && !$this->disabled ) {
- continue;
- }
- }
- $levels[] = $key;
- }
+ global $wgUser;
+
+ // If the form is disabled, display all relevant levels. Otherwise,
+ // just show the ones this user can use.
+ $levels = MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace(),
+ $this->disabled ? null : $wgUser
+ );
$id = 'mwProtect-level-' . $action;
$attribs = array(
}
}
- if ( $title->getNamespace() !== NS_MEDIAWIKI && $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() ) {
+ if ( $title->quickUserCan( 'protect', $user ) && $title->getRestrictionTypes() &&
+ MWNamespace::getRestrictionLevels( $title->getNamespace(), $user ) !== array( '' )
+ ) {
$mode = $title->isProtected() ? 'unprotect' : 'protect';
$content_navigation['actions'][$mode] = array(
'class' => ( $onPage && $action == $mode ) ? 'selected' : false,
? $config['mimeCallback']
: function ( $storagePath, $content, $fsPath ) {
// @TODO: handle the case of extension-less files using the contents
- return StreamFile::contentTypeFromPath( $storagePath ) ? : 'unknown/unknown';
+ return StreamFile::contentTypeFromPath( $storagePath ) ?: 'unknown/unknown';
};
$this->memCache = new EmptyBagOStuff(); // disabled by default
$this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
$status = Status::newGood(); // nothing to do
} elseif ( $this->params['src'] === $this->params['dst'] ) {
// Just update the destination file headers
- $headers = $this->getParam( 'headers' ) ? : array();
+ $headers = $this->getParam( 'headers' ) ?: array();
$status = $this->backend->describeInternal( $this->setFlags( array(
'src' => $this->params['dst'], 'headers' => $headers
) ) );
class LocalFile extends File {
const CACHE_FIELD_MAX_LEN = 1000;
- /**#@+
- * @private
- */
- var
- $fileExists, # does the file exist on disk? (loadFromXxx)
- $historyLine, # Number of line to return by nextHistoryLine() (constructor)
- $historyRes, # result of the query for the file's history (nextHistoryLine)
- $width, # \
- $height, # |
- $bits, # --- returned by getimagesize (loadFromXxx)
- $attr, # /
- $media_type, # MEDIATYPE_xxx (bitmap, drawing, audio...)
- $mime, # MIME type, determined by MimeMagic::guessMimeType
- $major_mime, # Major mime type
- $minor_mime, # Minor mime type
- $size, # Size in bytes (loadFromXxx)
- $metadata, # Handler-specific metadata
- $timestamp, # Upload timestamp
- $sha1, # SHA-1 base 36 content hash
- $user, $user_text, # User, who uploaded the file
- $description, # Description of current revision of the file
- $dataLoaded, # Whether or not core data has been loaded from the database (loadFromXxx)
- $extraDataLoaded, # Whether or not lazy-loaded data has been loaded from the database
- $upgraded, # Whether the row was upgraded on load
- $locked, # True if the image row is locked
- $lockedOwnTrx, # True if the image row is locked with a lock initiated transaction
- $missing, # True if file is not present in file system. Not to be cached in memcached
- $deleted; # Bitfield akin to rev_deleted
-
- /**#@-*/
-
- /**
- * @var LocalRepo
- */
- var $repo;
+ /** @var bool Does the file exist on disk? (loadFromXxx) */
+ protected $fileExists;
+ /** @var int image width */
+ protected $width;
+
+ /** @var int image height */
+ protected $height;
+
+ /** @var int Returned by getimagesize (loadFromXxx) */
+ protected $bits;
+
+ /** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
+ protected $media_type;
+
+ /** @var string MIME type, determined by MimeMagic::guessMimeType */
+ protected $mime;
+
+ /** @var int Size in bytes (loadFromXxx) */
+ protected $size;
+
+ /** @var string Handler-specific metadata */
+ protected $metadata;
+
+ /** @var string SHA-1 base 36 content hash */
+ protected $sha1;
+
+ /** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
+ protected $dataLoaded;
+
+ /** @var bool Whether or not lazy-loaded data has been loaded from the database */
+ protected $extraDataLoaded;
+
+ /** @var int Bitfield akin to rev_deleted */
+ protected $deleted;
+
+ /** @var string */
protected $repoClass = 'LocalRepo';
+ /** @var int Number of line to return by nextHistoryLine() (constructor) */
+ private $historyLine;
+
+ /** @var int Result of the query for the file's history (nextHistoryLine) */
+ private $historyRes;
+
+ /** @var string Major mime type */
+ private $major_mime;
+
+ /** @var string Minor mime type */
+ private $minor_mime;
+
+ /** @var string Upload timestamp */
+ private $timestamp;
+
+ /** @var int User ID of uploader */
+ private $user;
+
+ /** @var string User name of uploader */
+ private $user_text;
+
+ /** @var string Description of current revision of the file */
+ private $description;
+
+ /** @var bool Whether the row was upgraded on load */
+ private $upgraded;
+
+ /** @var bool True if the image row is locked */
+ private $locked;
+
+ /** @var bool True if the image row is locked with a lock initiated transaction */
+ private $lockedOwnTrx;
+
+ /** @var bool True if file is not present in file system. Not to be cached in memcached */
+ private $missing;
+
const LOAD_ALL = 1; // integer; load all the lazy fields too (like metadata)
/**
*/
class LocalFileDeleteBatch {
- /**
- * @var LocalFile
- */
- var $file;
+ /** @var LocalFile */
+ private $file;
+
+ /** @var string */
+ private $reason;
- var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
- var $status;
+ /** @var array */
+ private $srcRels = array();
+
+ /** @var array */
+ private $archiveUrls = array();
+
+ /** @var array Items to be processed in the deletion batch */
+ private $deletionBatch;
+
+ /** @var bool Wether to suppress all suppressable fields when deleting */
+ private $suppress;
+
+ /** @var FileRepoStatus */
+ private $status;
/**
* @param $file File
* @ingroup FileAbstraction
*/
class LocalFileRestoreBatch {
- /**
- * @var LocalFile
- */
- var $file;
+ /** @var LocalFile */
+ private $file;
+
+ /** @var array List of file IDs to restore */
+ private $cleanupBatch;
+
+ /** @var array List of file IDs to restore */
+ private $ids;
- var $cleanupBatch, $ids, $all, $unsuppress = false;
+ /** @var bool Add all revisions of the file */
+ private $all;
+
+ /** @var bool Wether to remove all settings for suppressed fields */
+ private $unsuppress = false;
/**
* @param $file File
* @ingroup FileAbstraction
*/
class LocalFileMoveBatch {
+ /** @var LocalFile */
+ protected $file;
- /**
- * @var LocalFile
- */
- var $file;
+ /** @var Title */
+ protected $target;
- /**
- * @var Title
- */
- var $target;
+ /** @var */
+ protected $cur;
+
+ /** @var */
+ protected $olds;
+
+ /** @var */
+ protected $oldCount;
- var $cur, $olds, $oldCount, $archive;
+ /** @var */
+ protected $archive;
/**
* @var DatabaseBase
*/
- var $db;
+ protected $db;
/**
* @param File $file
*
* Read-only.
*
- * TODO: Currently it doesn't really work in the repository role, there are
+ * @todo Currently it doesn't really work in the repository role, there are
* lots of functions missing. It is used by the WebStore extension in the
* standalone role.
*
* @ingroup FileAbstraction
*/
class UnregisteredLocalFile extends File {
- var $title, $path, $mime, $dims, $metadata;
+ /** @var Title */
+ protected $title;
- /**
- * @var MediaHandler
- */
- var $handler;
+ /** @var string */
+ protected $path;
+
+ /** @var bool|string */
+ protected $mime;
+
+ /** @var array Dimension data */
+ protected $dims;
+
+ /** @var bool|string Handler-specific metadata which will be saved in the img_metadata field */
+ protected $metadata;
+
+ /** @var MediaHandler */
+ public $handler;
/**
* @param string $path Storage path
'title' => $job->getTitle()->getDBkey(),
'params' => $job->getParams(),
// Some jobs cannot run until a "release timestamp"
- 'rtimestamp' => $job->getReleaseTimestamp() ? : 0,
+ 'rtimestamp' => $job->getReleaseTimestamp() ?: 0,
// Additional job metadata
'uuid' => UIDGenerator::newRawUUIDv4( UIDGenerator::QUICK_RAND ),
'sha1' => $job->ignoreDuplicates()
public function insert( IDatabase $dbw = null ) {
global $wgContLang;
- $dbw = $dbw ? : wfGetDB( DB_MASTER );
+ $dbw = $dbw ?: wfGetDB( DB_MASTER );
$id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
if ( $this->timestamp === null ) {
}
$cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName() );
+ $key = wfMemcKey( 'resourceloader', 'modulemodifiedhash', $this->getName(), $hash );
$data = $cache->get( $key );
if ( is_array( $data ) && $data['hash'] === $hash ) {
* @return string
*/
function convertPlural( $count, $forms ) {
+ $forms = $this->handleExplicitPluralForms( $count, $forms );
+ if ( is_string( $forms ) ) {
+ return $forms;
+ }
if ( !count( $forms ) ) {
return '';
}
'qbedit' => 'Edit',
'qbpageoptions' => 'This page',
'qbmyoptions' => 'My pages',
-'qbspecialpages' => 'Special pages',
'faq' => 'FAQ',
'faqpage' => 'Project:FAQ',
'sitetitle' => '{{SITENAME}}', # do not translate or duplicate this message to other languages
'perfcachedts' => 'The following data is cached, and was last updated $1. A maximum of {{PLURAL:$4|one result is|$4 results are}} available in the cache.',
'querypage-no-updates' => 'Updates for this page are currently disabled.
Data here will not presently be refreshed.',
-'wrong_wfQuery_params' => 'Incorrect parameters to wfQuery()<br />
-Function: $1<br />
-Query: $2',
'viewsource' => 'View source',
'viewsource-title' => 'View source for $1',
'actionthrottled' => 'Action throttled',
'exif-urgency-high' => 'High ($1)',
'exif-urgency-other' => 'User-defined priority ($1)',
-# External editor support
-'edit-externally' => 'Edit this file using an external application',
-'edit-externally-help' => '(See the [https://www.mediawiki.org/wiki/Manual:External_editors setup instructions] for more information)',
-
# 'all' in various places, this might be different for inflected languages
'watchlistall2' => 'all',
'namespacesall' => 'all',
public function finalSetup() {
parent::finalSetup();
- $this->regex = $this->getOption( 'regex' ) ? : $this->getOption( 'iregex' );
+ $this->regex = $this->getOption( 'regex' ) ?: $this->getOption( 'iregex' );
if ( $this->regex ) {
$this->regex = '/' . $this->regex . '/';
if ( $this->hasOption( 'iregex' ) ) {
'qbedit',
'qbpageoptions',
'qbmyoptions',
- 'qbspecialpages',
'faq',
'faqpage',
'sitetitle',
'perfcached',
'perfcachedts',
'querypage-no-updates',
- 'wrong_wfQuery_params',
'viewsource',
'viewsource-title',
'actionthrottled',
'exif-urgency-high',
'exif-urgency-other',
),
- 'edit-externally' => array(
- 'edit-externally',
- 'edit-externally-help',
- ),
'all' => array(
'watchlistall2',
'namespacesall',
'exif-maxaperturevalue' => '',
'exif-iimcategory' => '',
'exif-urgency' => '',
- 'edit-externally' => 'External editor support',
'all' => "'all' in various places, this might be different for inflected languages",
'confirmemail' => 'Email address confirmation',
'scarytransclusion' => 'Scary transclusion',
return form;
} );
+ if ( forms.length === 0 ) {
+ return '';
+ }
+
pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
if ( !pluralRules ) {
// default fallback.
$this->assertEquals( $result, $this->getLang()->convertPlural( $value, $forms ) );
}
+ /**
+ * Test explicit plural forms - n=FormN forms
+ * @covers Language::convertPlural
+ */
+ public function testExplicitPlural() {
+ $forms = array( 'one', 'few', 'many', 'other', '12=dozen' );
+ $this->assertEquals( 'dozen', $this->getLang()->convertPlural( 12, $forms ) );
+ $forms = array( 'one', 'few', 'many', '100=hundred', 'other', '12=dozen' );
+ $this->assertEquals( 'hundred', $this->getLang()->convertPlural( 100, $forms ) );
+ }
+
/**
* @dataProvider providePlural
* @covers Language::getPluralRuleType