* $wgPopularPasswordFile — The location of the default popular passwords file
has been moved to be in line with other non-PHP files used by libraries and
classes.
+* $wgEnableImageWhitelist is now disabled by default, as it opens up a hole for
+ potential privacy leaks by administrators. You can check
+ "MediaWiki:External image whitelist" on your wiki to see whether the feature
+ was ever used, and whether it needs to be re-enabled.
==== Removed configuration ====
* $wgEnableAPI and $wgEnableWriteAPI – These settings, deprecated in 1.31,
=== External library changes in 1.32 ===
==== New external libraries ====
-* Added wikimedia/xmp-reader 0.6.0.
-* Added Add pear/Net_SMTP 1.8.0.
-* …
+* Added pear/Net_SMTP v1.8.0.
+* Added wikimedia/xmp-reader v0.6.0.
+
+* Added cache/integration-tests v0.16.0 (dev-only).
+* Added giorgiosironi/eris v0.10.0 (dev-only).
+* Added seld/jsonlint v1.7.1 (dev-only).
+
+* Added EasyDeflate (unversioned).
==== Changed external libraries ====
-* Updated qunitjs from 2.4.0 to 2.6.2.
-* Updated wikimedia/scoped-callback from 1.0.0 to 2.0.0.
+* Updated OOUI from v0.26.3 to v0.29.2.
+* Updated wikimedia/base-convert from v1.0.1 to v2.0.0.
+* Updated wikimedia/remex-html from v1.0.3 to v2.0.1.
+* Updated wikimedia/scoped-callback from v1.0.0 to v2.0.0.
** ScopedCallback objects can no longer be serialized.
-* Updated wikimedia/wrappedstring from 2.3.0 to 3.0.1.
-* Updated mediawiki/mediawiki-codesniffer from v20.0.0 to v21.0.0.
-* Updated composer/spdx-licenses from 1.3.0 to 1.4.0.
-* Updated jquery.i18n from 1.0.4 to 1.0.5.
-* Updated wikimedia/timestamp from 1.0.0 to 2.2.0.
-* Updated wikimedia/remex-html from 1.0.3 to 2.0.1.
+* Updated wikimedia/timestamp from v1.0.0 to v2.2.0.
+* Updated wikimedia/wrappedstring from v2.3.0 to v3.0.1.
+
+* Updated composer/spdx-licenses from v1.3.0 to v1.4.0 (dev-only).
+* Updated mediawiki/mediawiki-codesniffer from v18.0.0 to v22.0.0 (dev-only).
+* Updated psy/psysh from v0.8.11 to v0.9.6 (dev-only).
+
+* Updated CLDRPluralRuleParser from v0.1.0 to v1.3.2-pre.
* Updated jquery from v3.2.1 to v3.3.1.
-* Updated wikimedia/base-convert from 1.0.1 to 2.0.0
+* Updated jquery.client from v2.0.0 to v2.0.1.
+* Updated jquery.i18n from v1.0.4 to v1.0.5.
+* Updated mustache.js from v0.8.2-d9aa703 to v1.0.0.
+* Updated OOjs from v2.2.0 to v2.2.2.
+* Updated qunitjs from v2.4.0 to v2.6.2.
+* Updated sinonjs from v1.17.3 to v1.17.7.
==== Removed external libraries ====
* pear/mail_mime-decode was removed.
-* …
=== Bug fixes in 1.32 ===
* SpecialPage::execute() will now only call checkLoginSecurityLevel() if
* ApiUsageException::getCodeString() (deprecated in 1.29)
* ApiUsageException::getMessageArray() (deprecated in 1.29)
* Class UsageException, deprecated in 1.29, has been removed.
+* MediaWiki no longer has a 'JavaScript-powered' wikitext toolbar built in. The
+ old "bulletin board style toolbar", known as "the 2006 wikitext editor", has
+ been removed, and instead sysadmins will be required to choose one (or more)
+ of the several extensions available for this purpose if they need the
+ functionality. The MediaWiki "tarball" releases have included the replacement
+ extension for this, the WikiEditor extension aka "the 2010 wikitext editor",
+ for many years now. As part of this, several parts of MediaWiki have been
+ removed or simplified:
+ * The user option 'showtoolbar' (shown as "Show edit toolbar") is no longer
+ available; if an extension adds a toolbar via the EditPageBeforeEditToolbar
+ hook, it will be shown; extensions should provide a specific user preference
+ to disable themselves as needed.
+ * The public methods Language::getImageFile() and ::getImageFiles(), and the
+ related specification of $imageFiles within individual languages' code file,
+ as well as the referenced static media assets, all of which were only used
+ inside MediaWiki itself for providing the icons for the old toolbar, have
+ been removed without explicit deprecation.
+ * The internal ResourceLoader module "mediawiki.toolbar", which is unused
+ except by MediaWiki itself and back-compatibility code, has been removed.
+ * The internal ResourceLoaderEditToolbarModule class has been removed.
=== Deprecations in 1.32 ===
* HTMLForm::setSubmitProgressive() is deprecated. No need to call it. Submit
== Compatibility ==
MediaWiki 1.32 requires PHP 7.0.0 or later. Although HHVM 3.18.5 or later is
supported, it is generally advised to use PHP 7.0.0 or later for long term
-support.
+support. MediaWiki requires that the mbstring, xml, ctype, json, iconv and
+fileinfo PHP extensions are loaded to work.
MySQL/MariaDB is the recommended DBMS. PostgreSQL or SQLite can also be used,
but support for them is somewhat less mature. There is experimental support for
=== Configuration changes in 1.33 ===
==== New configuration ====
+* $wgEnablePartialBlocks – This enables the Partial Blocks feature, which gives
+ accounts with block permissions the ability to block users, IPs, and IP ranges
+ from editing specific pages, while allowing them to edit the rest of the wiki.
* …
==== Changed configuration ====
* Language::truncate(), deprecated in 1.31, has been removed.
* UtfNormal, deprecated in 1.25, was removed. Use UtfNormal\Validator directly
instead.
+* (T197179) In OOUI HTMLForm fields, the parameters 'notice', 'notice-messages',
+ and 'notice-message', which were deprecated in 1.32, were removed. Instead,
+ use 'help', 'help-message', and 'help-messages'.
+* (T197179) HTMLFormField::getNotices(), deprecated in 1.32, was removed.
+* The "Parsoid v1" compatibility mappings in ParsoidVirtualRESTService and
+ RestbaseVirtualRESTService, deprecated since 1.26, have been removed.
+ Use the RESTBase v1 or Parsoid v3 API instead.
* …
=== Deprecations in 1.33 ===
'HTMLTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTextField.php',
'HTMLTextFieldWithButton' => __DIR__ . '/includes/htmlform/fields/HTMLTextFieldWithButton.php',
'HTMLTitleTextField' => __DIR__ . '/includes/htmlform/fields/HTMLTitleTextField.php',
+ 'HTMLTitlesMultiselectField' => __DIR__ . '/includes/htmlform/fields/HTMLTitlesMultiselectField.php',
'HTMLUserTextField' => __DIR__ . '/includes/htmlform/fields/HTMLUserTextField.php',
'HTMLUsersMultiselectField' => __DIR__ . '/includes/htmlform/fields/HTMLUsersMultiselectField.php',
'HTTPFileStreamer' => __DIR__ . '/includes/libs/filebackend/HTTPFileStreamer.php',
'MediaWiki\\Widget\\SelectWithInputWidget' => __DIR__ . '/includes/widget/SelectWithInputWidget.php',
'MediaWiki\\Widget\\SizeFilterWidget' => __DIR__ . '/includes/widget/SizeFilterWidget.php',
'MediaWiki\\Widget\\TitleInputWidget' => __DIR__ . '/includes/widget/TitleInputWidget.php',
+ 'MediaWiki\\Widget\\TitlesMultiselectWidget' => __DIR__ . '/includes/widget/TitlesMultiselectWidget.php',
'MediaWiki\\Widget\\UserInputWidget' => __DIR__ . '/includes/widget/UserInputWidget.php',
'MediaWiki\\Widget\\UsersMultiselectWidget' => __DIR__ . '/includes/widget/UsersMultiselectWidget.php',
'MemcLockManager' => __DIR__ . '/includes/libs/lockmanager/MemcLockManager.php',
'ResourceLoader' => __DIR__ . '/includes/resourceloader/ResourceLoader.php',
'ResourceLoaderClientHtml' => __DIR__ . '/includes/resourceloader/ResourceLoaderClientHtml.php',
'ResourceLoaderContext' => __DIR__ . '/includes/resourceloader/ResourceLoaderContext.php',
- 'ResourceLoaderEditToolbarModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderEditToolbarModule.php',
'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
'ResourceLoaderForeignApiModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderForeignApiModule.php',
"composer/semver": "1.4.2",
"cssjanus/cssjanus": "1.2.0",
"ext-ctype": "*",
+ "ext-fileinfo": "*",
"ext-iconv": "*",
"ext-json": "*",
"ext-mbstring": "*",
"suggest": {
"ext-apcu": "Local data cache for greatly improved performance",
"ext-curl": "Improved http communication abilities",
- "ext-fileinfo": "Improved mime magic detection",
"ext-intl": "ICU integration",
"ext-wikidiff2": "Diff accelerator",
"monolog/monolog": "Flexible debug logging system",
&$buttons: Array of edit buttons "Save", "Preview", "Live", and "Diff"
&$tabindex: HTML tabindex of the last edit check/button
-'EditPageBeforeEditToolbar': Allows modifying the edit toolbar above the
-textarea in the edit form.
-Hook subscribers can return false to avoid the default toolbar code being
-loaded.
-&$toolbar: The toolbar HTML
+'EditPageBeforeEditToolbar': Allow adding an edit toolbar above the textarea in
+the edit form.
+&$toolbar: The toolbar HTML, initially an empty `<div id="toolbar"></div>`
+Hook subscribers can return false to have no toolbar HTML be loaded.
'EditPageCopyrightWarning': Allow for site and per-namespace customization of
contribution/copyright notice.
public static function getAutoloadNamespaces() {
return [
'MediaWiki\\Auth\\' => __DIR__ . '/auth/',
+ 'MediaWiki\\Block\\' => __DIR__ . '/block/',
'MediaWiki\\Edit\\' => __DIR__ . '/edit/',
'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\Restriction;
use MediaWiki\MediaWikiServices;
class Block {
/** @var string|null */
private $systemBlockType;
+ /** @var bool */
+ private $isSitewide;
+
+ /** @var Restriction[] */
+ private $restrictions;
+
# TYPE constants
const TYPE_USER = 1;
const TYPE_IP = 2;
'allowUsertalk' => false,
'byText' => '',
'systemBlock' => null,
+ 'sitewide' => true,
];
if ( func_num_args() > 1 || !is_array( $options ) ) {
$this->mHideName = (bool)$options['hideName'];
$this->isHardblock( !$options['anonOnly'] );
$this->isAutoblocking( (bool)$options['enableAutoblock'] );
+ $this->isSitewide( (bool)$options['sitewide'] );
# Prevention measures
$this->prevents( 'sendemail', (bool)$options['blockEmail'] );
'ipb_block_email',
'ipb_allow_usertalk',
'ipb_parent_block_id',
+ 'ipb_sitewide',
] + CommentStore::getStore()->getFields( 'ipb_reason' );
}
'ipb_block_email',
'ipb_allow_usertalk',
'ipb_parent_block_id',
+ 'ipb_sitewide',
] + $commentQuery['fields'] + $actorQuery['fields'],
'joins' => $commentQuery['joins'] + $actorQuery['joins'],
];
&& $this->prevents( 'sendemail' ) == $block->prevents( 'sendemail' )
&& $this->prevents( 'editownusertalk' ) == $block->prevents( 'editownusertalk' )
&& $this->mReason == $block->mReason
+ && $this->isSitewide() == $block->isSitewide()
+ // Block::getRestrictions() may perform a database query, so keep it at
+ // the end.
+ && BlockRestriction::equals( $this->getRestrictions(), $block->getRestrictions() )
);
}
$this->isHardblock( !$row->ipb_anon_only );
$this->isAutoblocking( $row->ipb_enable_autoblock );
+ $this->isSitewide( (bool)$row->ipb_sitewide );
$this->prevents( 'createaccount', $row->ipb_create_account );
$this->prevents( 'sendemail', $row->ipb_block_email );
}
$dbw = wfGetDB( DB_MASTER );
+
+ BlockRestriction::deleteByParentBlockId( $this->getId() );
$dbw->delete( 'ipblocks', [ 'ipb_parent_block_id' => $this->getId() ], __METHOD__ );
+
+ BlockRestriction::deleteByBlockId( $this->getId() );
$dbw->delete( 'ipblocks', [ 'ipb_id' => $this->getId() ], __METHOD__ );
return $dbw->affectedRows() > 0;
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
$affected = $dbw->affectedRows();
- $this->mId = $dbw->insertId();
+ if ( $affected ) {
+ $this->setId( $dbw->insertId() );
+ if ( $this->restrictions ) {
+ BlockRestriction::insert( $this->restrictions );
+ }
+ }
# Don't collide with expired blocks.
# Do this after trying to insert to avoid locking.
);
if ( $ids ) {
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], __METHOD__ );
+ BlockRestriction::deleteByBlockId( $ids );
$dbw->insert( 'ipblocks', $row, __METHOD__, [ 'IGNORE' ] );
$affected = $dbw->affectedRows();
- $this->mId = $dbw->insertId();
+ $this->setId( $dbw->insertId() );
+ if ( $this->restrictions ) {
+ BlockRestriction::insert( $this->restrictions );
+ }
}
}
$dbw->startAtomic( __METHOD__ );
- $dbw->update(
+ $result = $dbw->update(
'ipblocks',
$this->getDatabaseArray( $dbw ),
[ 'ipb_id' => $this->getId() ],
__METHOD__
);
- $affected = $dbw->affectedRows();
+ // Only update the restrictions if they have been modified.
+ if ( $this->restrictions !== null ) {
+ // An empty array should remove all of the restrictions.
+ if ( empty( $this->restrictions ) ) {
+ $success = BlockRestriction::deleteByBlockId( $this->getId() );
+ } else {
+ $success = BlockRestriction::update( $this->restrictions );
+ }
+ // Update the result. The first false is the result, otherwise, true.
+ $result = $result && $success;
+ }
if ( $this->isAutoblocking() ) {
// update corresponding autoblock(s) (T50813)
[ 'ipb_parent_block_id' => $this->getId() ],
__METHOD__
);
+
+ // Only update the restrictions if they have been modified.
+ if ( $this->restrictions !== null ) {
+ BlockRestriction::updateByParentBlockId( $this->getId(), $this->restrictions );
+ }
} else {
// autoblock no longer required, delete corresponding autoblock(s)
+ BlockRestriction::deleteByParentBlockId( $this->getId() );
$dbw->delete(
'ipblocks',
[ 'ipb_parent_block_id' => $this->getId() ],
$dbw->endAtomic( __METHOD__ );
- if ( $affected ) {
+ if ( $result ) {
$auto_ipd_ids = $this->doRetroactiveAutoblock();
return [ 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ];
}
- return false;
+ return $result;
}
/**
'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite
'ipb_block_email' => $this->prevents( 'sendemail' ),
'ipb_allow_usertalk' => !$this->prevents( 'editownusertalk' ),
- 'ipb_parent_block_id' => $this->mParentBlockId
+ 'ipb_parent_block_id' => $this->mParentBlockId,
+ 'ipb_sitewide' => $this->isSitewide(),
] + CommentStore::getStore()->insert( $dbw, 'ipb_reason', $this->mReason )
+ ActorMigration::newMigration()->getInsertValues( $dbw, 'ipb_by', $this->getBlocker() );
$autoblock->mHideName = $this->mHideName;
$autoblock->prevents( 'editownusertalk', $this->prevents( 'editownusertalk' ) );
$autoblock->mParentBlockId = $this->mId;
+ $autoblock->isSitewide( $this->isSitewide() );
+ $autoblock->setRestrictions( $this->getRestrictions() );
if ( $this->mExpiry == 'infinity' ) {
# Original block was indefinite, start an autoblock now
return $this->mId;
}
+ /**
+ * Set the block ID
+ *
+ * @param int $blockId
+ * @return int
+ */
+ private function setId( $blockId ) {
+ $this->mId = (int)$blockId;
+
+ if ( is_array( $this->restrictions ) ) {
+ $this->restrictions = BlockRestriction::setBlockId( $blockId, $this->restrictions );
+ }
+
+ return $this;
+ }
+
/**
* Get the system block type, if any
* @since 1.29
: false;
}
+ /**
+ * Indicates that the block is a sitewide block. This means the user is
+ * prohibited from editing any page on the site (other than their own talk
+ * page).
+ *
+ * @param null|bool $x
+ * @return bool
+ */
+ public function isSitewide( $x = null ) {
+ return wfSetVar( $this->isSitewide, $x );
+ }
+
/**
* Get/set whether the Block prevents a given action
*
* @return bool|null Null for unrecognized rights.
*/
public function prevents( $action, $x = null ) {
- global $wgBlockDisablesLogin;
+ $config = RequestContext::getMain()->getConfig();
+ $blockDisablesLogin = $config->get( 'BlockDisablesLogin' );
+ $blockAllowsUTEdit = $config->get( 'BlockAllowsUTEdit' );
+
$res = null;
switch ( $action ) {
case 'edit':
case 'sendemail':
$res = wfSetVar( $this->mBlockEmail, $x );
break;
+ case 'upload':
+ // Until T6995 is completed
+ $res = $this->isSitewide();
+ break;
case 'editownusertalk':
$res = wfSetVar( $this->mDisableUsertalk, $x );
+ // edit own user talk can be disabled by config
+ if ( !$blockAllowsUTEdit ) {
+ $res = true;
+ }
break;
case 'read':
$res = false;
break;
}
- if ( !$res && $wgBlockDisablesLogin ) {
+ if ( !$res && $blockDisablesLogin ) {
// If a block would disable login, then it should
// prevent any action that all users cannot do
$anon = new User;
$fname
);
if ( $ids ) {
+ BlockRestriction::deleteByBlockId( $ids );
$dbw->delete( 'ipblocks', [ 'ipb_id' => $ids ], $fname );
}
}
* @return array
*/
public function getPermissionsError( IContextSource $context ) {
+ $params = $this->getBlockErrorParams( $context );
+
+ $msg = 'blockedtext';
+ if ( $this->getSystemBlockType() !== null ) {
+ $msg = 'systemblockedtext';
+ } elseif ( $this->mAuto ) {
+ $msg = 'autoblockedtext';
+ } elseif ( !$this->isSitewide() ) {
+ $msg = 'blockedtext-partial';
+ }
+
+ array_unshift( $params, $msg );
+
+ return $params;
+ }
+
+ /**
+ * Get block information used in different block error messages
+ *
+ * @param IContextSource $context
+ * @return array
+ */
+ public function getBlockErrorParams( IContextSource $context ) {
$blocker = $this->getBlocker();
if ( $blocker instanceof User ) { // local user
$blockerUserpage = $blocker->getUserPage();
/* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
* This could be a username, an IP range, or a single IP. */
$intended = $this->getTarget();
-
$systemBlockType = $this->getSystemBlockType();
-
$lang = $context->getLanguage();
+
return [
- $systemBlockType !== null
- ? 'systemblockedtext'
- : ( $this->mAuto ? 'autoblockedtext' : 'blockedtext' ),
$link,
$reason,
$context->getRequest()->getIP(),
$lang->userTimeAndDate( $this->mTimestamp, $context->getUser() ),
];
}
+
+ /**
+ * Get Restrictions.
+ *
+ * Getting the restrictions will perform a database query if the restrictions
+ * are not already loaded.
+ *
+ * @return Restriction[]
+ */
+ public function getRestrictions() {
+ if ( $this->restrictions === null ) {
+ // If the block id has not been set, then do not attempt to load the
+ // restrictions.
+ if ( !$this->mId ) {
+ return [];
+ }
+ $this->restrictions = BlockRestriction::loadByBlockId( $this->mId );
+ }
+
+ return $this->restrictions;
+ }
+
+ /**
+ * Set Restrictions.
+ *
+ * @param Restriction[] $restrictions
+ *
+ * @return self
+ */
+ public function setRestrictions( array $restrictions ) {
+ $this->restrictions = array_filter( $restrictions, function ( $restriction ) {
+ return $restriction instanceof Restriction;
+ } );
+
+ return $this;
+ }
+
+ /**
+ * Checks if a block prevents an edit on a given article
+ *
+ * @param \Title $title
+ * @return bool
+ */
+ public function preventsEdit( \Title $title ) {
+ $blocked = $this->isSitewide();
+
+ // user talk page has it's own rules
+ // This check happens before partial blocks because the flag
+ // to allow user to edit their user talk page could be
+ // overwritten by a partial block restriction (E.g. user talk namespace)
+ $user = $this->getTarget();
+ if ( $title->equals( $user->getTalkPage() ) ) {
+ $blocked = $this->prevents( 'editownusertalk' );
+ }
+
+ if ( !$this->isSitewide() ) {
+ $restrictions = $this->getRestrictions();
+ foreach ( $restrictions as $restriction ) {
+ if ( $restriction->matches( $title ) ) {
+ $blocked = true;
+ }
+ }
+ }
+
+ return $blocked;
+ }
}
*
* Set this to true to enable the on-wiki whitelist (MediaWiki:External image whitelist)
* Or false to disable it
+ *
+ * @since 1.14
*/
-$wgEnableImageWhitelist = true;
+$wgEnableImageWhitelist = false;
/**
* A different approach to the above: simply allow the "<img>" tag to be used.
'rows' => 25, // @deprecated since 1.29 No longer used in core
'showhiddencats' => 0,
'shownumberswatching' => 1,
- 'showtoolbar' => 1,
'skin' => false,
'stubthreshold' => 0,
'thumbsize' => 5,
*/
$wgTagStatisticsNewTable = false;
+/**
+ * Flag to enable Partial Blocks. This allows an admin to prevent a user from editing specific pages
+ * or namespaces.
+ *
+ * @since 1.32
+ * @deprecated 1.32
+ * @var bool
+ */
+$wgEnablePartialBlocks = false;
+
/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker
$out->addModuleStyles( 'mediawiki.editfont.styles' );
$user = $this->context->getUser();
- if ( $user->getOption( 'showtoolbar' ) ) {
- // The addition of default buttons is handled by getEditToolbar() which
- // has its own dependency on this module. The call here ensures the module
- // is loaded in time (it has position "top") for other modules to register
- // buttons (e.g. extensions, gadgets, user scripts).
- $out->addModules( 'mediawiki.toolbar' );
- }
if ( $user->getOption( 'uselivepreview' ) ) {
$out->addModules( 'mediawiki.action.edit.preview' );
$out->addHTML( $this->editFormTextTop );
- $showToolbar = true;
if ( $this->wasDeletedSinceLastEdit() ) {
- if ( $this->formtype == 'save' ) {
- // Hide the toolbar and edit area, user can click preview to get it back
- // Add an confirmation checkbox and explanation.
- $showToolbar = false;
- } else {
+ if ( $this->formtype !== 'save' ) {
$out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
'deletedwhileediting' );
}
$out->addHTML( $editConflictHelper->getEditFormHtmlBeforeContent() );
}
- if ( !$this->mTitle->isUserConfigPage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
+ if ( !$this->mTitle->isUserConfigPage() ) {
$out->addHTML( self::getEditToolbar( $this->mTitle ) );
}
}
/**
- * Shows a bulletin board style toolbar for common editing functions.
- * It can be disabled in the user preferences.
+ * Allow extensions to provide a toolbar.
*
* @param Title|null $title Title object for the page being edited (optional)
- * @return string
+ * @return string|null
*/
public static function getEditToolbar( $title = null ) {
- global $wgOut, $wgEnableUploads, $wgForeignFileRepos;
-
- $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
- $showSignature = true;
- if ( $title ) {
- $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
- }
-
- $contLang = MediaWikiServices::getInstance()->getContentLanguage();
-
- /**
- * $toolarray is an array of arrays each of which includes the
- * opening tag, the closing tag, optionally a sample text that is
- * inserted between the two when no selection is highlighted
- * and. The tip text is shown when the user moves the mouse
- * over the button.
- *
- * Images are defined in ResourceLoaderEditToolbarModule.
- */
- $toolarray = [
- [
- 'id' => 'mw-editbutton-bold',
- 'open' => '\'\'\'',
- 'close' => '\'\'\'',
- 'sample' => wfMessage( 'bold_sample' )->text(),
- 'tip' => wfMessage( 'bold_tip' )->text(),
- ],
- [
- 'id' => 'mw-editbutton-italic',
- 'open' => '\'\'',
- 'close' => '\'\'',
- 'sample' => wfMessage( 'italic_sample' )->text(),
- 'tip' => wfMessage( 'italic_tip' )->text(),
- ],
- [
- 'id' => 'mw-editbutton-link',
- 'open' => '[[',
- 'close' => ']]',
- 'sample' => wfMessage( 'link_sample' )->text(),
- 'tip' => wfMessage( 'link_tip' )->text(),
- ],
- [
- 'id' => 'mw-editbutton-extlink',
- 'open' => '[',
- 'close' => ']',
- 'sample' => wfMessage( 'extlink_sample' )->text(),
- 'tip' => wfMessage( 'extlink_tip' )->text(),
- ],
- [
- 'id' => 'mw-editbutton-headline',
- 'open' => "\n== ",
- 'close' => " ==\n",
- 'sample' => wfMessage( 'headline_sample' )->text(),
- 'tip' => wfMessage( 'headline_tip' )->text(),
- ],
- $imagesAvailable ? [
- 'id' => 'mw-editbutton-image',
- 'open' => '[[' . $contLang->getNsText( NS_FILE ) . ':',
- 'close' => ']]',
- 'sample' => wfMessage( 'image_sample' )->text(),
- 'tip' => wfMessage( 'image_tip' )->text(),
- ] : false,
- $imagesAvailable ? [
- 'id' => 'mw-editbutton-media',
- 'open' => '[[' . $contLang->getNsText( NS_MEDIA ) . ':',
- 'close' => ']]',
- 'sample' => wfMessage( 'media_sample' )->text(),
- 'tip' => wfMessage( 'media_tip' )->text(),
- ] : false,
- [
- 'id' => 'mw-editbutton-nowiki',
- 'open' => "<nowiki>",
- 'close' => "</nowiki>",
- 'sample' => wfMessage( 'nowiki_sample' )->text(),
- 'tip' => wfMessage( 'nowiki_tip' )->text(),
- ],
- $showSignature ? [
- 'id' => 'mw-editbutton-signature',
- 'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
- 'close' => '',
- 'sample' => '',
- 'tip' => wfMessage( 'sig_tip' )->text(),
- ] : false,
- [
- 'id' => 'mw-editbutton-hr',
- 'open' => "\n----\n",
- 'close' => '',
- 'sample' => '',
- 'tip' => wfMessage( 'hr_tip' )->text(),
- ]
- ];
-
- $script = '';
- foreach ( $toolarray as $tool ) {
- if ( !$tool ) {
- continue;
- }
-
- $params = [
- // Images are defined in ResourceLoaderEditToolbarModule
- false,
- // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
- // Older browsers show a "speedtip" type message only for ALT.
- // Ideally these should be different, realistically they
- // probably don't need to be.
- $tool['tip'],
- $tool['open'],
- $tool['close'],
- $tool['sample'],
- $tool['id'],
- ];
+ $startingToolbar = '<div id="toolbar"></div>';
+ $toolbar = $startingToolbar;
- $script .= Xml::encodeJsCall(
- 'mw.toolbar.addButton',
- $params,
- ResourceLoader::inDebugMode()
- );
- }
-
- $toolbar = '<div id="toolbar"></div>';
-
- if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
- // Only add the old toolbar cruft to the page payload if the toolbar has not
- // been over-written by a hook caller
- $nonce = $wgOut->getCSPNonce();
- $wgOut->addScript( Html::inlineScript(
- ResourceLoader::makeInlineCodeWithModule( 'mediawiki.toolbar', $script ),
- $nonce
- ) );
+ if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
+ return null;
};
-
- return $toolbar;
+ // Don't add a pointless `<div>` to the page unless a hook caller populated it
+ return ( $toolbar === $startingToolbar ) ? null : $toolbar;
}
/**
private $config;
/**
- * @var String Cache what action this request is
+ * @var string Cache what action this request is
*/
private $action;
* @return CryptRand
*/
public function getCryptRand() {
+ wfDeprecated( __METHOD__, '1.32' );
return $this->getService( 'CryptRand' );
}
* or else addWikiTextAsContent() if $interface is false.
*/
public function addWikiText( $text, $linestart = true, $interface = true ) {
+ wfDeprecated( __METHOD__, '1.32' );
$title = $this->getTitle();
if ( !$title ) {
throw new MWException( 'Title is null' );
) {
global $wgParser;
+ if ( !$tidy ) {
+ wfDeprecated( 'disabling tidy', '1.32' );
+ }
+
$popts = $this->parserOptions();
$oldTidy = $popts->setTidy( $tidy );
$popts->setInterfaceMessage( (bool)$interface );
* Helper function to setup the PHP implementation of OOUI to use in this request.
*
* @since 1.26
- * @param String $skinName The Skin name to determine the correct OOUI theme
- * @param String $dir Language direction
+ * @param string $skinName The Skin name to determine the correct OOUI theme
+ * @param string $dir Language direction
*/
public static function setupOOUI( $skinName = 'default', $dir = 'ltr' ) {
$themes = ResourceLoaderOOUIModule::getSkinThemeMap();
'blocked',
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
) );
+ } elseif ( is_array( $error ) && $error[0] === 'blockedtext-partial' && $user->getBlock() ) {
+ $status->fatal( ApiMessage::create(
+ 'apierror-blocked-partial',
+ 'blocked',
+ [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
+ ) );
} elseif ( is_array( $error ) && $error[0] === 'autoblockedtext' && $user->getBlock() ) {
$status->fatal( ApiMessage::create(
'apierror-autoblocked',
'autoblocked',
[ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
);
+ } elseif ( !$block->isSitewide() ) {
+ $this->dieWithError(
+ 'apierror-blocked-partial',
+ 'blocked',
+ [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $block ) ]
+ );
} else {
$this->dieWithError(
'apierror-blocked',
}
}
+ $editingRestriction = 'sitewide';
+ $pageRestrictions = '';
+ if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
+ if ( $params['pagerestrictions'] ) {
+ $count = count( $params['pagerestrictions'] );
+ if ( $count > 10 ) {
+ $this->dieWithError(
+ $this->msg(
+ 'apierror-integeroutofrange-abovebotmax',
+ 'pagerestrictions',
+ 10,
+ $count
+ )
+ );
+ }
+ }
+
+ if ( $params['partial'] ) {
+ $editingRestriction = 'partial';
+ }
+
+ $pageRestrictions = implode( "\n", $params['pagerestrictions'] );
+ }
+
if ( $params['userid'] !== null ) {
$username = User::whoIs( $params['userid'] );
'Watch' => $params['watchuser'],
'Confirm' => true,
'Tags' => $params['tags'],
+ 'EditingRestriction' => $editingRestriction,
+ 'PageRestrictions' => $pageRestrictions,
];
$retval = SpecialBlock::processForm( $data, $this->getContext() );
$res['allowusertalk'] = $params['allowusertalk'];
$res['watchuser'] = $params['watchuser'];
+ if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
+ $res['partial'] = $params['partial'];
+ $res['pagerestrictions'] = $params['pagerestrictions'];
+ }
+
$this->getResult()->addValue( null, $this->getModuleName(), $res );
}
}
public function getAllowedParams() {
- return [
+ $params = [
'user' => [
ApiBase::PARAM_TYPE => 'user',
],
ApiBase::PARAM_ISMULTI => true,
],
];
+
+ if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
+ $params['partial'] = false;
+ $params['pagerestrictions'] = [
+ ApiBase::PARAM_ISMULTI => true,
+ ];
+ }
+
+ return $params;
}
public function needsToken() {
// obvious that this is even possible.
// @codeCoverageIgnoreStart
case EditPage::AS_BLOCKED_PAGE_FOR_USER:
- $this->dieWithError(
- 'apierror-blocked',
- 'blocked',
- [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ]
- );
+ $this->dieBlocked( $user->getBlock() );
case EditPage::AS_READ_ONLY_PAGE:
$this->dieReadOnly();
* This mimicks the behavior of EditPage in formatting a summary
*
* @param Title $title of the page being parsed
- * @param Array $params the API parameters of the request
+ * @param array $params The API parameters of the request
* @return Content|bool
*/
private function formatSummary( $title, $params ) {
* @file
*/
+use Wikimedia\Rdbms\IResultWrapper;
+use MediaWiki\Block\BlockRestriction;
+
/**
* Query module to enumerate all user blocks
*
$fld_reason = isset( $prop['reason'] );
$fld_range = isset( $prop['range'] );
$fld_flags = isset( $prop['flags'] );
+ $fld_restrictions = isset( $prop['restrictions'] );
$result = $this->getResult();
$this->addFieldsIf( 'ipb_expiry', $fld_expiry );
$this->addFieldsIf( [ 'ipb_range_start', 'ipb_range_end' ], $fld_range );
$this->addFieldsIf( [ 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock',
- 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk' ],
+ 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk', 'ipb_sitewide' ],
$fld_flags );
+ $this->addFieldsIf( 'ipb_sitewide', $fld_restrictions );
if ( $fld_reason ) {
$commentQuery = $commentStore->getJoin( 'ipb_reason' );
$res = $this->select( __METHOD__ );
+ $restrictions = [];
+ if ( $fld_restrictions ) {
+ $restrictions = $this->getRestrictionData( $res, $params['limit'] );
+ }
+
$count = 0;
foreach ( $res as $row ) {
if ( ++$count > $params['limit'] ) {
$block['noemail'] = (bool)$row->ipb_block_email;
$block['hidden'] = (bool)$row->ipb_deleted;
$block['allowusertalk'] = (bool)$row->ipb_allow_usertalk;
+ $block['partial'] = !(bool)$row->ipb_sitewide;
+ }
+
+ if ( $fld_restrictions ) {
+ $block['restrictions'] = [];
+ if ( !$row->ipb_sitewide && isset( $restrictions[$row->ipb_id] ) ) {
+ $block['restrictions'] = $restrictions[$row->ipb_id];
+ }
}
+
$fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $block );
if ( !$fit ) {
$this->setContinueEnumParameter( 'continue', "$row->ipb_timestamp|$row->ipb_id" );
return $name;
}
+ /**
+ * Retrieves the restrictions based on the query result.
+ *
+ * @param IResultWrapper $result
+ * @param int $limit
+ *
+ * @return array
+ */
+ private static function getRestrictionData( IResultWrapper $result, $limit ) {
+ $partialIds = [];
+ $count = 0;
+ foreach ( $result as $row ) {
+ if ( ++$count <= $limit && !$row->ipb_sitewide ) {
+ $partialIds[] = (int)$row->ipb_id;
+ }
+ }
+
+ $restrictions = BlockRestriction::loadByBlockId( $partialIds );
+
+ $data = [];
+ $keys = [
+ 'page' => 'pages',
+ 'ns' => 'namespaces',
+ ];
+ foreach ( $restrictions as $restriction ) {
+ $key = $keys[$restriction->getType()];
+ $id = $restriction->getBlockId();
+ switch ( $restriction->getType() ) {
+ case 'page':
+ $value = [ 'id' => $restriction->getValue() ];
+ self::addTitleInfo( $value, $restriction->getTitle() );
+ break;
+ default:
+ $value = $restriction->getValue();
+ }
+
+ if ( !isset( $data[$id][$key] ) ) {
+ $data[$id][$key] = [];
+ ApiResult::setIndexedTagName( $data[$id][$key], $restriction->getType() );
+ }
+ $data[$id][$key][] = $value;
+ }
+
+ return $data;
+ }
+
public function getAllowedParams() {
$blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' );
'expiry',
'reason',
'range',
- 'flags'
+ 'flags',
+ 'restrictions',
],
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
$this->limit = 1;
}
}
- if ( isset( $params['section'] ) ) {
- $this->section = $params['section'];
- } else {
- $this->section = false;
- }
+ $this->section = $params['section'] ?? false;
}
$userMax = $this->parseContent ? 1 : ( $smallLimit ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
$vals['blockreason'] = $block->mReason;
$vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->mTimestamp );
$vals['blockexpiry'] = ApiResult::formatExpiry( $block->getExpiry(), 'infinite' );
+ $vals['blockpartial'] = !$block->isSitewide();
if ( $block->getSystemBlockType() !== null ) {
$vals['systemblocktype'] = $block->getSystemBlockType();
}
$this->dieStatus( $this->errorArrayToStatus( $retval, $user ) );
}
- $watch = 'preferences';
- if ( isset( $params['watchlist'] ) ) {
- $watch = $params['watchlist'];
- }
+ $watch = $params['watchlist'] ?? 'preferences';
// Watch pages
$this->setWatch( $watch, $titleObj, 'watchrollback' );
"apihelp-block-param-reblock": "If the user is already blocked, overwrite the existing block.",
"apihelp-block-param-watchuser": "Watch the user's or IP address's user and talk pages.",
"apihelp-block-param-tags": "Change tags to apply to the entry in the block log.",
+ "apihelp-block-param-partial": "Block user from specific pages or namespaces rather than the entire site.",
+ "apihelp-block-param-pagerestrictions": "List of titles to block the user from editing. Only applies when 'partial' is set to true.",
"apihelp-block-example-ip-simple": "Block IP address <kbd>192.0.2.5</kbd> for three days with reason <kbd>First strike</kbd>.",
"apihelp-block-example-user-complex": "Block user <kbd>Vandal</kbd> indefinitely with reason <kbd>Vandalism</kbd>, and prevent new account creation and email sending.",
"apihelp-query+blocks-paramvalue-prop-reason": "Adds the reason given for the block.",
"apihelp-query+blocks-paramvalue-prop-range": "Adds the range of IP addresses affected by the block.",
"apihelp-query+blocks-paramvalue-prop-flags": "Tags the ban with (autoblock, anononly, etc.).",
+ "apihelp-query+blocks-paramvalue-prop-restrictions": "Adds the partial block restrictions if the block is not sitewide.",
"apihelp-query+blocks-param-show": "Show only items that meet these criteria.\nFor example, to see only indefinite blocks on IP addresses, set <kbd>$1show=ip|!temp</kbd>.",
"apihelp-query+blocks-example-simple": "List blocks.",
"apihelp-query+blocks-example-users": "List blocks of users <kbd>Alice</kbd> and <kbd>Bob</kbd>.",
"apierror-bad-watchlist-token": "Incorrect watchlist token provided. Please set a correct token in [[Special:Preferences]].",
"apierror-blockedfrommail": "You have been blocked from sending email.",
"apierror-blocked": "You have been blocked from editing.",
+ "apierror-blocked-partial": "You have been blocked from editing this page.",
"apierror-botsnotsupported": "This interface is not supported for bots.",
"apierror-cannot-async-upload-file": "The parameters <var>async</var> and <var>file</var> cannot be combined. If you want asynchronous processing of your uploaded file, first upload it to stash (using the <var>stash</var> parameter) and then publish the stashed file asynchronously (using <var>filekey</var> and <var>async</var>).",
"apierror-cannotreauthenticate": "This action is not available as your identity cannot be verified.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Fornece o modo como o título da página é exibido.",
"apihelp-query+info-paramvalue-prop-varianttitles": "Fornece o título de apresentação em todas as variantes da língua de conteúdo da wiki.",
"apihelp-query+info-param-testactions": "Testa se o usuário atual pode executar determinadas ações na página.",
+ "apihelp-query+info-param-testactionsdetail": "Nível de detalhe de <var>$1testactions</var>. Use os parâmetros <var>errorformat</var> e <var>errorlang</var> do [[Special:ApiHelp/main|módulo principal]] para controlar o formato das mensagens devolvidas.",
"apihelp-query+info-paramvalue-testactionsdetail-boolean": "Retorna um valor booleano para cada ação.",
"apihelp-query+info-paramvalue-testactionsdetail-full": "Retornar mensagens descrevendo por que a ação não é permitida ou uma matriz vazia, se for permitida.",
"apihelp-query+info-paramvalue-testactionsdetail-quick": "Como <kbd>completo</kbd>, mas pulando verificação de caros.",
"apihelp-query+info-paramvalue-prop-notificationtimestamp": "A data e hora das notificações de alterações de cada página vigiada.",
"apihelp-query+info-paramvalue-prop-subjectid": "O identificador da página progenitora de cada página de discussão.",
"apihelp-query+info-paramvalue-prop-url": "Fornece um URL completo, um URL de edição e o URL canónico, para cada página.",
- "apihelp-query+info-paramvalue-prop-readable": "Indica se o utilizador pode ler esta página.",
+ "apihelp-query+info-paramvalue-prop-readable": "Indica se o utilizador pode ler esta página. Em vez deste parâmetro, use <kbd>intestactions=read</kbd>.",
"apihelp-query+info-paramvalue-prop-preload": "Fornece o texto devolvido por EditFormPreloadText.",
"apihelp-query+info-paramvalue-prop-displaytitle": "Fornece a forma como o título da página é apresentado.",
"apihelp-query+info-paramvalue-prop-varianttitles": "Fornece o título de apresentação em todas as variantes da língua de conteúdo da wiki.",
"apihelp-query+info-param-testactions": "Testar se o utilizador pode realizar certas operações na página.",
+ "apihelp-query+info-param-testactionsdetail": "Nível de detalhe de <var>$1testactions</var>. Use os parâmetros <var>errorformat</var> e <var>errorlang</var> do [[Special:ApiHelp/main|módulo principal]] para controlar o formato das mensagens devolvidas.",
+ "apihelp-query+info-paramvalue-testactionsdetail-boolean": "Devolver um valor booliano para cada ação.",
+ "apihelp-query+info-paramvalue-testactionsdetail-full": "Devolver mensagens que descrevem porque a ação não é permitida, ou uma matriz vazia se ela for permitida.",
+ "apihelp-query+info-paramvalue-testactionsdetail-quick": "Como <kbd>full</kbd> mas saltando verificações exigentes.",
"apihelp-query+info-param-token": "Em substituição, usar [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].",
"apihelp-query+info-example-simple": "Obter informações sobre a página <kbd>Main Page</kbd>.",
"apihelp-query+info-example-protection": "Obter informação geral e de proteção sobre a página <kbd>Main Page</kbd>.",
"apierror-assertnameduserfailed": "A asserção de que o utilizador é \"$1\" falhou.",
"apierror-assertuserfailed": "A asserção de que o utilizador está autenticado falhou.",
"apierror-autoblocked": "O seu endereço IP foi bloqueado automaticamente, porque foi usado por um utilizador bloqueado.",
+ "apierror-bad-badfilecontexttitle": "Título inválido no parâmetro <var>$1badfilecontexttitle</var>.",
"apierror-badconfig-resulttoosmall": "O valor de <code>$wgAPIMaxResultSize</code> nesta wiki é demasiado pequeno para conter informação básica de resultados.",
"apierror-badcontinue": "Parâmetro de continuação inválido. Deve passar o valor original devolvido pela consulta anterior.",
"apierror-baddiff": "Não foi possível obter a lista de diferenças. Uma das revisões, ou ambas, não existem, ou não tem permissão para vê-las.",
"apihelp-block-param-reblock": "{{doc-apihelp-param|block|reblock}}",
"apihelp-block-param-watchuser": "{{doc-apihelp-param|block|watchuser}}",
"apihelp-block-param-tags": "{{doc-apihelp-param|block|tags}}",
+ "apihelp-block-param-partial": "{{doc-apihelp-param|block|partial}}",
+ "apihelp-block-param-pagerestrictions": "{{doc-apihelp-param|block|pagerestrictions}}",
"apihelp-block-example-ip-simple": "{{doc-apihelp-example|block}}",
"apihelp-block-example-user-complex": "{{doc-apihelp-example|block}}",
"apihelp-changeauthenticationdata-summary": "{{doc-apihelp-summary|changeauthenticationdata}}",
"apihelp-query+blocks-paramvalue-prop-reason": "{{doc-apihelp-paramvalue|query+blocks|prop|reason}}",
"apihelp-query+blocks-paramvalue-prop-range": "{{doc-apihelp-paramvalue|query+blocks|prop|range}}",
"apihelp-query+blocks-paramvalue-prop-flags": "{{doc-apihelp-paramvalue|query+blocks|prop|flags}}",
+ "apihelp-query+blocks-paramvalue-prop-restrictions": "{{doc-apihelp-paramvalue|query+blocks|prop|flags}}",
"apihelp-query+blocks-param-show": "{{doc-apihelp-param|query+blocks|show}}",
"apihelp-query+blocks-example-simple": "{{doc-apihelp-example|query+blocks}}",
"apihelp-query+blocks-example-users": "{{doc-apihelp-example|query+blocks}}",
"apierror-bad-watchlist-token": "{{doc-apierror}}",
"apierror-blockedfrommail": "{{doc-apierror}}",
"apierror-blocked": "{{doc-apierror}}",
+ "apierror-blocked-partial": "{{doc-apierror}}",
"apierror-botsnotsupported": "{{doc-apierror}}",
"apierror-cannot-async-upload-file": "{{doc-apierror}}",
"apierror-cannotreauthenticate": "{{doc-apierror}}",
}
public function getDomain() {
- if ( isset( $this->domain ) ) {
- return $this->domain;
- } else {
- return 'invaliddomain';
- }
+ return $this->domain ?? 'invaliddomain';
}
public function validDomain( $domain ) {
--- /dev/null
+<?php
+/**
+ * Block restriction interface.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Block;
+
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\Block\Restriction\Restriction;
+use Wikimedia\Rdbms\IResultWrapper;
+use Wikimedia\Rdbms\IDatabase;
+
+class BlockRestriction {
+
+ /**
+ * Retrieves the restrictions from the database by block id.
+ *
+ * @param int|array $blockId
+ * @param IDatabase|null $db
+ * @param array $options Options to pass to the select query.
+ * @return Restriction[]
+ */
+ public static function loadByBlockId( $blockId, IDatabase $db = null ) {
+ if ( is_null( $blockId ) || $blockId === [] ) {
+ return [];
+ }
+
+ $db = $db ?: wfGetDb( DB_REPLICA );
+
+ $result = $db->select(
+ [ 'ipblocks_restrictions', 'page' ],
+ [ 'ir_ipb_id', 'ir_type', 'ir_value', 'page_namespace', 'page_title' ],
+ [ 'ir_ipb_id' => $blockId ],
+ __METHOD__,
+ [],
+ [ 'page' => [ 'LEFT JOIN', [ 'ir_type' => PageRestriction::TYPE_ID, 'ir_value=page_id' ] ] ]
+ );
+
+ return self::resultToRestrictions( $result );
+ }
+
+ /**
+ * Inserts the restrictions into the database.
+ *
+ * @param Restriction[] $restrictions
+ * @return bool
+ */
+ public static function insert( array $restrictions ) {
+ if ( empty( $restrictions ) ) {
+ return false;
+ }
+
+ $rows = [];
+ foreach ( $restrictions as $restriction ) {
+ if ( !$restriction instanceof Restriction ) {
+ continue;
+ }
+ $rows[] = $restriction->toRow();
+ }
+
+ if ( empty( $rows ) ) {
+ return false;
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ return $dbw->insert(
+ 'ipblocks_restrictions',
+ $rows,
+ __METHOD__,
+ [ 'IGNORE' ]
+ );
+ }
+
+ /**
+ * Updates the list of restrictions. This method does not allow removing all
+ * of the restrictions. To do that, use ::deleteByBlockId().
+ *
+ * @param Restriction[] $restrictions
+ * @return bool
+ */
+ public static function update( array $restrictions ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->startAtomic( __METHOD__ );
+
+ // Organize the restrictions by blockid.
+ $restrictionList = self::restrictionsByBlockId( $restrictions );
+
+ // Load the existing restrictions and organize by block id. Any block ids
+ // that were passed into this function will be used to load all of the
+ // existing restrictions. This list might be the same, or may be completely
+ // different.
+ $existingList = [];
+ $blockIds = array_keys( $restrictionList );
+ if ( !empty( $blockIds ) ) {
+ $result = $dbw->select(
+ [ 'ipblocks_restrictions', 'page' ],
+ [ 'ir_ipb_id', 'ir_type', 'ir_value' ],
+ [ 'ir_ipb_id' => $blockIds ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+
+ $existingList = self::restrictionsByBlockId(
+ self::resultToRestrictions( $result )
+ );
+ }
+
+ $result = true;
+ // Perform the actions on a per block-id basis.
+ foreach ( $restrictionList as $blockId => $blockRestrictions ) {
+ // Insert all of the restrictions first, ignoring ones that already exist.
+ $success = self::insert( $blockRestrictions );
+
+ // Update the result. The first false is the result, otherwise, true.
+ $result = $success && $result;
+
+ $restrictionsToRemove = self::restrictionsToRemove(
+ $existingList[$blockId] ?? [],
+ $restrictions
+ );
+
+ // Nothing to remove.
+ if ( empty( $restrictionsToRemove ) ) {
+ continue;
+ }
+
+ $success = self::delete( $restrictionsToRemove );
+
+ // Update the result. The first false is the result, otherwise, true.
+ $result = $success && $result;
+ }
+
+ $dbw->endAtomic( __METHOD__ );
+
+ return $result;
+ }
+
+ /**
+ * Updates the list of restrictions by parent id.
+ *
+ * @param int $parentBlockId
+ * @param Restriction[] $restrictions
+ * @return bool
+ */
+ public static function updateByParentBlockId( $parentBlockId, array $restrictions ) {
+ // If removing all of the restrictions, then just delete them all.
+ if ( empty( $restrictions ) ) {
+ return self::deleteByParentBlockId( $parentBlockId );
+ }
+
+ $parentBlockId = (int)$parentBlockId;
+
+ $db = wfGetDb( DB_MASTER );
+
+ $db->startAtomic( __METHOD__ );
+
+ $blockIds = $db->selectFieldValues(
+ 'ipblocks',
+ 'ipb_id',
+ [ 'ipb_parent_block_id' => $parentBlockId ],
+ __METHOD__,
+ [ 'FOR UPDATE' ]
+ );
+
+ $result = true;
+ foreach ( $blockIds as $id ) {
+ $success = self::update( self::setBlockId( $id, $restrictions ) );
+ // Update the result. The first false is the result, otherwise, true.
+ $result = $success && $result;
+ }
+
+ $db->endAtomic( __METHOD__ );
+
+ return $result;
+ }
+
+ /**
+ * Delete the restrictions.
+ *
+ * @param Restriction[]|null $restrictions
+ * @throws MWException
+ * @return bool
+ */
+ public static function delete( array $restrictions ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $result = true;
+ foreach ( $restrictions as $restriction ) {
+ if ( !$restriction instanceof Restriction ) {
+ continue;
+ }
+
+ $success = $dbw->delete(
+ 'ipblocks_restrictions',
+ // The restriction row is made up of a compound primary key. Therefore,
+ // the row and the delete conditions are the same.
+ $restriction->toRow(),
+ __METHOD__
+ );
+ // Update the result. The first false is the result, otherwise, true.
+ $result = $success && $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Delete the restrictions by Block ID.
+ *
+ * @param int|array $blockId
+ * @throws MWException
+ * @return bool
+ */
+ public static function deleteByBlockId( $blockId ) {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->delete(
+ 'ipblocks_restrictions',
+ [ 'ir_ipb_id' => $blockId ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * Delete the restrictions by Parent Block ID.
+ *
+ * @param int|array $parentBlockId
+ * @throws MWException
+ * @return bool
+ */
+ public static function deleteByParentBlockId( $parentBlockId ) {
+ $dbw = wfGetDB( DB_MASTER );
+ return $dbw->deleteJoin(
+ 'ipblocks_restrictions',
+ 'ipblocks',
+ 'ir_ipb_id',
+ 'ipb_id',
+ [ 'ipb_parent_block_id' => $parentBlockId ],
+ __METHOD__
+ );
+ }
+
+ /**
+ * Checks if two arrays of Restrictions are effectively equal. This is a loose
+ * equality check as the restrictions do not have to contain the same block
+ * ids.
+ *
+ * @param Restriction[] $a
+ * @param Restriction[] $b
+ * @return bool
+ */
+ public static function equals( array $a, array $b ) {
+ $filter = function ( $restriction ) {
+ return $restriction instanceof Restriction;
+ };
+
+ // Ensure that every item in the array is a Restriction. This prevents a
+ // fatal error from calling Restriction::getHash if something in the array
+ // is not a restriction.
+ $a = array_filter( $a, $filter );
+ $b = array_filter( $b, $filter );
+
+ $aCount = count( $a );
+ $bCount = count( $b );
+
+ // If the count is different, then they are obviously a different set.
+ if ( $aCount !== $bCount ) {
+ return false;
+ }
+
+ // If both sets contain no items, then they are the same set.
+ if ( $aCount === 0 && $bCount === 0 ) {
+ return true;
+ }
+
+ $hasher = function ( $r ) {
+ return $r->getHash();
+ };
+
+ $aHashes = array_map( $hasher, $a );
+ $bHashes = array_map( $hasher, $b );
+
+ sort( $aHashes );
+ sort( $bHashes );
+
+ return $aHashes === $bHashes;
+ }
+
+ /**
+ * Set the blockId on a set of restrictions and return a new set.
+ *
+ * @param int $blockId
+ * @param Restriction[] $restrictions
+ * @return Restriction[]
+ */
+ public static function setBlockId( $blockId, array $restrictions ) {
+ $blockRestrictions = [];
+
+ foreach ( $restrictions as $restriction ) {
+ if ( !$restriction instanceof Restriction ) {
+ continue;
+ }
+
+ // Clone the restriction so any references to the current restriction are
+ // not suddenly changed to a different blockId.
+ $restriction = clone $restriction;
+ $restriction->setBlockId( $blockId );
+
+ $blockRestrictions[] = $restriction;
+ }
+
+ return $blockRestrictions;
+ }
+
+ /**
+ * Get the restrictions that should be removed, which are existing
+ * restrictions that are not in the new list of restrictions.
+ *
+ * @param Restriction[] $existing
+ * @param Restriction[] $new
+ * @return array
+ */
+ private static function restrictionsToRemove( array $existing, array $new ) {
+ return array_filter( $existing, function ( $e ) use ( $new ) {
+ foreach ( $new as $restriction ) {
+ if ( !$restriction instanceof Restriction ) {
+ continue;
+ }
+
+ if ( $restriction->equals( $e ) ) {
+ return false;
+ }
+ }
+
+ return true;
+ } );
+ }
+
+ /**
+ * Converts an array of restrictions to an associative array of restrictions
+ * where the keys are the block ids.
+ *
+ * @param Restriction[] $restrictions
+ * @return array
+ */
+ private static function restrictionsByBlockId( array $restrictions ) {
+ $blockRestrictions = [];
+
+ foreach ( $restrictions as $restriction ) {
+ // Ensure that all of the items in the array are restrictions.
+ if ( !$restriction instanceof Restriction ) {
+ continue;
+ }
+
+ if ( !isset( $blockRestrictions[$restriction->getBlockId()] ) ) {
+ $blockRestrictions[$restriction->getBlockId()] = [];
+ }
+
+ $blockRestrictions[$restriction->getBlockId()][] = $restriction;
+ }
+
+ return $blockRestrictions;
+ }
+
+ /**
+ * Convert an Result Wrapper to an array of restrictions.
+ *
+ * @param IResultWrapper $result
+ * @return Restriction[]
+ */
+ private static function resultToRestrictions( IResultWrapper $result ) {
+ $restrictions = [];
+ foreach ( $result as $row ) {
+ $restriction = self::rowToRestriction( $row );
+
+ if ( !$restriction ) {
+ continue;
+ }
+
+ $restrictions[] = $restriction;
+ }
+
+ return $restrictions;
+ }
+
+ /**
+ * Convert a result row from the database into a restriction object.
+ *
+ * @param \stdClass $row
+ * @return Restriction|null
+ */
+ private static function rowToRestriction( \stdClass $row ) {
+ switch ( $row->ir_type ) {
+ case PageRestriction::TYPE_ID:
+ return PageRestriction::newFromRow( $row );
+ default:
+ return null;
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * Abstract block restriction.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Block\Restriction;
+
+abstract class AbstractRestriction implements Restriction {
+
+ /**
+ * @var int
+ */
+ protected $blockId;
+
+ /**
+ * @var int
+ */
+ protected $value;
+
+ /**
+ * Create Restriction.
+ *
+ * @param int $blockId
+ * @param int $value
+ */
+ public function __construct( $blockId, $value ) {
+ $this->blockId = (int)$blockId;
+ $this->value = (int)$value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBlockId() {
+ return $this->blockId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBlockId( $blockId ) {
+ $this->blockId = (int)$blockId;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue() {
+ return $this->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function newFromRow( \stdClass $row ) {
+ return new static( $row->ir_ipb_id, $row->ir_value );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toRow() {
+ return [
+ 'ir_ipb_id' => $this->getBlockId(),
+ 'ir_type' => $this->getTypeId(),
+ 'ir_value' => $this->getValue(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function equals( Restriction $other ) {
+ return $this->getHash() === $other->getHash();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHash() {
+ return $this->getType() . '-' . $this->getValue();
+ }
+}
--- /dev/null
+<?php
+/**
+ * A Block restriction object of type 'Page'.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Block\Restriction;
+
+class PageRestriction extends AbstractRestriction {
+
+ const TYPE = 'page';
+ const TYPE_ID = 1;
+
+ /**
+ * @var \Title
+ */
+ protected $title;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function matches( \Title $title ) {
+ return $title->equals( $this->getTitle() );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType() {
+ return self::TYPE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypeId() {
+ return self::TYPE_ID;
+ }
+
+ /**
+ * Set the title.
+ *
+ * @param \Title $title
+ * @return self
+ */
+ public function setTitle( \Title $title ) {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ /**
+ * Get Title.
+ *
+ * @return \Title|null
+ */
+ public function getTitle() {
+ if ( !$this->title ) {
+ $this->title = \Title::newFromID( $this->value );
+ }
+
+ return $this->title;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function newFromRow( \stdClass $row ) {
+ $restriction = parent::newFromRow( $row );
+
+ // If the page_namespace and the page_title were provided, add the title to
+ // the restriction.
+ if ( isset( $row->page_namespace ) && isset( $row->page_title ) ) {
+ // Clone the row so it is not mutated.
+ $row = clone $row;
+ $row->page_id = $row->ir_value;
+ $title = \Title::newFromRow( $row );
+ $restriction->setTitle( $title );
+ }
+
+ return $restriction;
+ }
+}
--- /dev/null
+<?php
+/**
+ * Block restriction interface.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Block\Restriction;
+
+interface Restriction {
+
+ /**
+ * Gets the id of the block.
+ *
+ * @return int
+ */
+ public function getBlockId();
+
+ /**
+ * Sets the id of the block.
+ *
+ * @param int $blockId
+ * @return self
+ */
+ public function setBlockId( $blockId );
+
+ /**
+ * Gets the value of the restriction.
+ *
+ * @return int
+ */
+ public function getValue();
+
+ /**
+ * Gets the type of restriction
+ *
+ * @return string
+ */
+ public function getType();
+
+ /**
+ * Gets the id of the type of restriction. This id is used in the database.
+ *
+ * @return string
+ */
+ public function getTypeId();
+
+ /**
+ * Creates a new Restriction from a database row.
+ *
+ * @return self
+ */
+ public static function newFromRow( \stdClass $row );
+
+ /**
+ * Convert a restriction object into a row array for insertion.
+ *
+ * @return array
+ */
+ public function toRow();
+
+ /**
+ * Determine if a restriction matches a given title.
+ *
+ * @param \Title $title
+ * @return bool
+ */
+ public function matches( \Title $title );
+
+ /**
+ * Determine if a restriction equals another restriction.
+ *
+ * @param Restriction $other
+ * @return bool
+ */
+ public function equals( Restriction $other );
+
+ /**
+ * Create a unique hash of the block restriction based on the type and value.
+ *
+ * @return string
+ */
+ public function getHash();
+
+}
/**
* Returns check key for the backlinks cache for a particular title
*
- * @return String
+ * @return string
*/
private function makeCheckKey() {
return $this->wanCache->makeKey(
// either.
$po = ParserOptions::newFromAnon();
$po->setAllowUnsafeRawHtml( false );
+ $po->setTidy( true );
return $po;
}
// from malicious sources. As a precaution, disable
// the <html> parser tag when parsing messages.
$this->mParserOptions->setAllowUnsafeRawHtml( false );
+ // For the same reason, tidy the output!
+ $this->mParserOptions->setTidy( true );
}
return $this->mParserOptions;
function __construct( $conf = [] ) {
global $wgCacheDirectory;
- if ( isset( $conf['directory'] ) ) {
- $this->directory = $conf['directory'];
- } else {
- $this->directory = $wgCacheDirectory;
- }
+ $this->directory = $conf['directory'] ?? $wgCacheDirectory;
}
public function get( $code, $key ) {
public function __construct( $conf = [] ) {
global $wgCacheDirectory;
- if ( isset( $conf['directory'] ) ) {
- $this->directory = $conf['directory'];
- } else {
- $this->directory = $wgCacheDirectory;
- }
+ $this->directory = $conf['directory'] ?? $wgCacheDirectory;
}
public function startWrite( $code ) {
$this->showHide = $filterDefinition['showHide'];
}
- if ( isset( $filterDefinition['isReplacedInStructuredUi'] ) ) {
- $this->isReplacedInStructuredUi = $filterDefinition['isReplacedInStructuredUi'];
- } else {
- $this->isReplacedInStructuredUi = false;
- }
+ $this->isReplacedInStructuredUi = $filterDefinition['isReplacedInStructuredUi'] ?? false;
if ( isset( $filterDefinition['default'] ) ) {
$this->setDefault( $filterDefinition['default'] );
$this->queryCallable = $filterDefinition['queryCallable'];
}
- if ( isset( $filterDefinition['activeValue'] ) ) {
- $this->activeValue = $filterDefinition['activeValue'];
- } else {
- $this->activeValue = true;
- }
+ $this->activeValue = $filterDefinition['activeValue'] ?? true;
}
/**
}
$this->type = $groupDefinition['type'];
- if ( isset( $groupDefinition['priority'] ) ) {
- $this->priority = $groupDefinition['priority'];
- } else {
- $this->priority = self::DEFAULT_PRIORITY;
- }
+ $this->priority = $groupDefinition['priority'] ?? self::DEFAULT_PRIORITY;
$this->isFullCoverage = $groupDefinition['isFullCoverage'];
foreach ( $tagsToAdd as $tag ) {
$changeTagMapping[$tag] = $changeTagDefStore->acquireId( $tag );
}
-
- $dbw->update(
- 'change_tag_def',
- [ 'ctd_count = ctd_count + 1' ],
- [ 'ctd_name' => $tagsToAdd ],
- __METHOD__
- );
+ // T207881: update the counts at the end of the transaction
+ $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tagsToAdd ) {
+ $dbw->update(
+ 'change_tag_def',
+ [ 'ctd_count = ctd_count + 1' ],
+ [ 'ctd_name' => $tagsToAdd ],
+ __METHOD__
+ );
+ } );
}
$tagsRows = [];
);
$dbw->delete( 'change_tag', $conds, __METHOD__ );
if ( $dbw->affectedRows() && $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) {
- $dbw->update(
- 'change_tag_def',
- [ 'ctd_count = ctd_count - 1' ],
- [ 'ctd_name' => $tag ],
- __METHOD__
- );
-
- $dbw->delete(
- 'change_tag_def',
- [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
- __METHOD__
- );
+ // T207881: update the counts at the end of the transaction
+ $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $tag ) {
+ $dbw->update(
+ 'change_tag_def',
+ [ 'ctd_count = ctd_count - 1' ],
+ [ 'ctd_name' => $tag ],
+ __METHOD__
+ );
+
+ $dbw->delete(
+ 'change_tag_def',
+ [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ],
+ __METHOD__
+ );
+ } );
}
}
}
* @param array $records List of records to append
*/
protected function addMessages( $channel, array $records ) {
- if ( isset( $this->options['alias'][$channel] ) ) {
- $topic = $this->options['alias'][$channel];
- } else {
- $topic = "monolog_$channel";
- }
+ $topic = $this->options['alias'][$channel] ?? "monolog_$channel";
$partition = $this->getRandomPartition( $topic );
if ( $partition !== null ) {
$this->produce->setMessages( $topic, $partition, $records );
const TEXT = 0;
const STUB = 1;
- const BATCH_SIZE = 1000;
+ const BATCH_SIZE = 50000;
/** @var int */
public $text;
} elseif ( $this->history & self::FULL ) {
# Full history dumps...
# query optimization for history stub dumps
- if ( $this->text == self::STUB && $orderRevs ) {
+ if ( $this->text == self::STUB ) {
$tables = $revQuery['tables'];
+ $opts[] = 'STRAIGHT_JOIN';
$opts['USE INDEX']['revision'] = 'rev_page_id';
unset( $join['revision'] );
$join['page'] = [ 'INNER JOIN', 'rev_page=page_id' ];
* 'help-inline' -- Whether help text (defined using options above) will be shown
* inline after the input field, rather than in a popup.
* Defaults to true. Only used by OOUI form fields.
- * 'notice' -- (deprecated, use 'help' instead)
- * 'notice-messages' -- (deprecated, use 'help-messages' instead)
- * 'notice-message' -- (deprecated, use 'help-message' instead)
* 'required' -- passed through to the object, indicating that it
* is a required field.
* 'size' -- the length of text fields
'title' => HTMLTitleTextField::class,
'user' => HTMLUserTextField::class,
'usersmultiselect' => HTMLUsersMultiselectField::class,
+ 'titlesmultiselect' => HTMLTitlesMultiselectField::class,
];
public $mFieldData;
if ( isset( $params['hide-if'] ) ) {
$this->mHideIf = $params['hide-if'];
}
-
- if ( isset( $this->mParams['notice-message'] ) ) {
- wfDeprecated( "'notice-message' parameter in HTMLForm", '1.32' );
- }
- if ( isset( $this->mParams['notice-messages'] ) ) {
- wfDeprecated( "'notice-messages' parameter in HTMLForm", '1.32' );
- }
- if ( isset( $this->mParams['notice'] ) ) {
- wfDeprecated( "'notice' parameter in HTMLForm", '1.32' );
- }
}
/**
$error = new OOUI\HtmlSnippet( $error );
}
- $notices = $this->getNotices( 'skip deprecation' );
- foreach ( $notices as &$notice ) {
- $notice = new OOUI\HtmlSnippet( $notice );
- }
-
$config = [
'classes' => [ "mw-htmlform-field-$fieldType", $this->mClass ],
'align' => $this->getLabelAlignOOUI(),
'help' => ( $help !== null && $help !== '' ) ? new OOUI\HtmlSnippet( $help ) : null,
'errors' => $errors,
- 'notices' => $notices,
'infusable' => $infusable,
'helpInline' => $this->isHelpInline(),
];
return $errors;
}
- /**
- * Determine notices to display for the field.
- *
- * @since 1.28
- * @deprecated since 1.32
- * @param string $skipDeprecation Pass 'skip deprecation' to avoid the deprecation
- * warning (since 1.32)
- * @return string[]
- */
- public function getNotices( $skipDeprecation = null ) {
- if ( $skipDeprecation !== 'skip deprecation' ) {
- wfDeprecated( __METHOD__, '1.32' );
- }
-
- $notices = [];
-
- if ( isset( $this->mParams['notice-message'] ) ) {
- $notices[] = $this->getMessage( $this->mParams['notice-message'] )->parse();
- }
-
- if ( isset( $this->mParams['notice-messages'] ) ) {
- foreach ( $this->mParams['notice-messages'] as $msg ) {
- $notices[] = $this->getMessage( $msg )->parse();
- }
- } elseif ( isset( $this->mParams['notice'] ) ) {
- $notices[] = $this->mParams['notice'];
- }
-
- return $notices;
- }
-
/**
* @return string HTML
*/
/**
* Combines the passed element with a button.
- * @param String $element Element to combine the button with.
- * @return String
+ * @param string $element Element to combine the button with.
+ * @return string
*/
public function getElement( $element ) {
return $element . "\u{00A0}" . $this->getInputHTML( '' );
}
public function getDefault() {
- if ( isset( $this->mDefault ) ) {
- return $this->mDefault;
- } else {
- return [];
- }
+ return $this->mDefault ?? [];
}
public function filterDataForSubmit( $data ) {
--- /dev/null
+<?php
+
+use MediaWiki\Widget\TitlesMultiselectWidget;
+
+/**
+ * Implements a tag multiselect input field for titles.
+ *
+ * Besides the parameters recognized by HTMLTitleTextField, additional recognized
+ * parameters are:
+ * default - (optional) Array of usernames to use as preset data
+ * placeholder - (optional) Custom placeholder message for input
+ *
+ * The result is the array of titles
+ *
+ * This widget is a duplication of HTMLUsersMultiselectField, except for:
+ * - The configuration variable changed to 'titles' (from 'users')
+ * - OOUI modules were adjusted for the TitlesMultiselectWidget
+ * - The PHP version instantiates a MediaWiki\Widget\TitlesMultiselectWidget
+ *
+ * @note This widget is not likely to remain functional in non-OOUI forms.
+ */
+class HTMLTitlesMultiselectField extends HTMLTitleTextField {
+ public function __construct( $params ) {
+ $params += [
+ // This overrides the default from HTMLTitleTextField
+ 'required' => false,
+ ];
+
+ parent::__construct( $params );
+ }
+
+ public function loadDataFromRequest( $request ) {
+ $value = $request->getText( $this->mName, $this->getDefault() );
+
+ $titlesArray = explode( "\n", $value );
+ // Remove empty lines
+ $titlesArray = array_values( array_filter( $titlesArray, function ( $title ) {
+ return trim( $title ) !== '';
+ } ) );
+ // This function is expected to return a string
+ return implode( "\n", $titlesArray );
+ }
+
+ public function validate( $value, $alldata ) {
+ if ( !$this->mParams['exists'] ) {
+ return true;
+ }
+
+ if ( is_null( $value ) ) {
+ return false;
+ }
+
+ // $value is a string, because HTMLForm fields store their values as strings
+ $titlesArray = explode( "\n", $value );
+
+ if ( isset( $this->mParams['max'] ) ) {
+ if ( count( $titlesArray ) > $this->mParams['max'] ) {
+ return $this->msg( 'htmlform-int-toohigh', $this->mParams['max'] );
+ }
+ }
+
+ foreach ( $titlesArray as $title ) {
+ $result = parent::validate( $title, $alldata );
+ if ( $result !== true ) {
+ return $result;
+ }
+ }
+
+ return true;
+ }
+
+ public function getInputHTML( $value ) {
+ $this->mParent->getOutput()->enableOOUI();
+ return $this->getInputOOUI( $value );
+ }
+
+ public function getInputOOUI( $value ) {
+ $params = [
+ 'id' => $this->mID,
+ 'name' => $this->mName,
+ 'dir' => $this->mDir,
+ ];
+
+ if ( isset( $this->mParams['disabled'] ) ) {
+ $params['disabled'] = $this->mParams['disabled'];
+ }
+
+ if ( isset( $this->mParams['default'] ) ) {
+ $params['default'] = $this->mParams['default'];
+ }
+
+ if ( isset( $this->mParams['placeholder'] ) ) {
+ $params['placeholder'] = $this->mParams['placeholder'];
+ } else {
+ $params['placeholder'] = $this->msg( 'mw-widgets-titlesmultiselect-placeholder' )->plain();
+ }
+
+ if ( !is_null( $value ) ) {
+ // $value is a string, but the widget expects an array
+ $params['default'] = $value === '' ? [] : explode( "\n", $value );
+ }
+
+ // Make the field auto-infusable when it's used inside a legacy HTMLForm rather than OOUIHTMLForm
+ $params['infusable'] = true;
+ $params['classes'] = [ 'mw-htmlform-field-autoinfuse' ];
+ $widget = new TitlesMultiselectWidget( $params );
+ $widget->setAttributes( [ 'data-mw-modules' => implode( ',', $this->getOOUIModules() ) ] );
+
+ return $widget;
+ }
+
+ protected function shouldInfuseOOUI() {
+ return true;
+ }
+
+ protected function getOOUIModules() {
+ return [ 'mediawiki.widgets.TitlesMultiselectWidget' ];
+ }
+
+}
$this->url = wfExpandUrl( $url, PROTO_HTTP );
$this->parsedUrl = wfParseUrl( $this->url );
- if ( isset( $options['logger'] ) ) {
- $this->logger = $options['logger'];
- } else {
- $this->logger = new NullLogger();
- }
+ $this->logger = $options['logger'] ?? new NullLogger();
if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
$this->status = StatusValue::newFatal( 'http-invalid-url', $url );
$revision->setText( $text );
}
- if ( isset( $revisionInfo['timestamp'] ) ) {
- $revision->setTimestamp( $revisionInfo['timestamp'] );
- } else {
- $revision->setTimestamp( wfTimestampNow() );
- }
+ $revision->setTimestamp( $revisionInfo['timestamp'] ?? wfTimestampNow() );
if ( isset( $revisionInfo['comment'] ) ) {
$revision->setComment( $revisionInfo['comment'] );
// This will automatically add "AutoloadClasses" to $wgAutoloadClasses
$data = $registry->readFromQueue( $queue );
- $hooks = [];
- if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
- $hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'];
- }
+ $hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ?? [];
if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
$hooks = array_merge_recursive( $hooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
}
* @return mixed
*/
public function getVar( $name, $default = null ) {
- if ( !isset( $this->settings[$name] ) ) {
- return $default;
- } else {
- return $this->settings[$name];
- }
+ return $this->settings[$name] ?? $default;
}
/**
$data = $registry->readFromQueue( $queue );
$wgAutoloadClasses += $data['autoload'];
- $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
- /** @suppress PhanUndeclaredVariable $wgHooks is set by DefaultSettings */
- $wgHooks['LoadExtensionSchemaUpdates'] : [];
+ /** @suppress PhanUndeclaredVariable $wgHooks is set by DefaultSettings */
+ $hooksWeWant = $wgHooks['LoadExtensionSchemaUpdates'] ?? [];
if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
$hooksWeWant = array_merge_recursive(
$existingSchema = false;
$this->parent->showMessage( 'config-unknown-collation' );
}
- if ( isset( $row->Engine ) ) {
- $existingEngine = $row->Engine;
- } else {
- $existingEngine = $row->Type;
- }
+ $existingEngine = $row->Engine ?? $row->Type;
}
} else {
$existingSchema = false;
return $this->session;
}
- if ( isset( $session['happyPages'] ) ) {
- $this->happyPages = $session['happyPages'];
- } else {
- $this->happyPages = [];
- }
+ $this->happyPages = $session['happyPages'] ?? [];
- if ( isset( $session['skippedPages'] ) ) {
- $this->skippedPages = $session['skippedPages'];
- } else {
- $this->skippedPages = [];
- }
+ $this->skippedPages = $session['skippedPages'] ?? [];
$lowestUnhappy = $this->getLowestUnhappy();
* @return array
*/
public function getSession( $name, $default = null ) {
- if ( !isset( $this->session[$name] ) ) {
- return $default;
- } else {
- return $this->session[$name];
- }
+ return $this->session[$name] ?? $default;
}
/**
if ( !isset( $params['labelAttribs'] ) ) {
$params['labelAttribs'] = [];
}
- if ( isset( $params['rawtext'] ) ) {
- $labelText = $params['rawtext'];
- } else {
- $labelText = $this->parse( wfMessage( $params['label'] )->text() );
- }
+ $labelText = $params['rawtext'] ?? $this->parse( wfMessage( $params['label'] )->text() );
return "<div class=\"config-input-check\">\n" .
$params['help'] .
public function getRadioSet( $params ) {
$items = $this->getRadioElements( $params );
- if ( !isset( $params['label'] ) ) {
- $label = '';
- } else {
- $label = $params['label'];
- }
+ $label = $params['label'] ?? '';
if ( !isset( $params['controlName'] ) ) {
$params['controlName'] = 'config_' . $params['var'];
return $status;
}
- if ( isset( $vars['wgDBadminuser'] ) ) {
- $this->setVar( '_InstallUser', $vars['wgDBadminuser'] );
- } else {
- $this->setVar( '_InstallUser', $vars['wgDBuser'] );
- }
- if ( isset( $vars['wgDBadminpassword'] ) ) {
- $this->setVar( '_InstallPassword', $vars['wgDBadminpassword'] );
- } else {
- $this->setVar( '_InstallPassword', $vars['wgDBpassword'] );
- }
+ $this->setVar( '_InstallUser', $vars['wgDBadminuser'] ?? $vars['wgDBuser'] );
+ $this->setVar( '_InstallPassword', $vars['wgDBadminpassword'] ?? $vars['wgDBpassword'] );
// Test the database connection
$status = $installer->getConnection();
// config-license-cc-0, config-license-pd, config-license-gfdl, config-license-none,
// config-license-cc-choose
$entry = $this->parent->licenses[$code];
- if ( isset( $entry['text'] ) ) {
- $this->setVar( 'wgRightsText', $entry['text'] );
- } else {
- $this->setVar( 'wgRightsText', wfMessage( 'config-license-' . $code )->text() );
- }
+ $this->setVar( 'wgRightsText',
+ $entry['text'] ?? wfMessage( 'config-license-' . $code )->text() );
$this->setVar( 'wgRightsUrl', $entry['url'] );
$this->setVar( 'wgRightsIcon', $entry['icon'] );
} else {
"config-help": "ajuda",
"config-help-tooltip": "clique para expandir",
"config-nofile": "O arquivo \"$1\" não foi encontrado. Ele foi apagado?",
- "config-extension-link": "Você sabia que sua wiki suporta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensões]?\n\nVocê pode explorar as [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensões por categoria] ou visitar a [https://www.mediawiki.org/wiki/Extension_Matrix Matriz de Extensões] para ver a lista completa.",
+ "config-extension-link": "Você sabia que sua wiki suporta [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions extensões]?\n\nVocê pode explorar as [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category extensões por categoria]",
"config-skins-screenshots": "$1 (screenshots: $2)",
"config-extensions-requires": "$1 (requer $2)",
"config-screenshot": "screenshot",
global $wgJobTypeConf;
$conf = [ 'wiki' => $this->wiki, 'type' => $type ];
- if ( isset( $wgJobTypeConf[$type] ) ) {
- $conf = $conf + $wgJobTypeConf[$type];
- } else {
- $conf = $conf + $wgJobTypeConf['default'];
- }
+ $conf += $wgJobTypeConf[$type] ?? $wgJobTypeConf['default'];
$conf['aggregator'] = JobQueueAggregator::singleton();
if ( !isset( $conf['readOnlyReason'] ) ) {
$conf['readOnlyReason'] = $this->readOnlyReason;
public static function getMimeType( $file ) {
// Infer the MIME-type from the file extension
$ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
- if ( isset( self::$mimeTypes[$ext] ) ) {
- return self::$mimeTypes[$ext];
- }
-
- return mime_content_type( realpath( $file ) );
+ return self::$mimeTypes[$ext] ?? mime_content_type( realpath( $file ) );
}
/**
$this->expires = strtotime( $attr['expires'] );
}
- if ( isset( $attr['path'] ) ) {
- $this->path = $attr['path'];
- } else {
- $this->path = '/';
- }
+ $this->path = $attr['path'] ?? '/';
if ( isset( $attr['domain'] ) ) {
if ( self::validateCookieDomain( $attr['domain'] ) ) {
* @return string
*/
protected function initialRandomState() {
+ wfDeprecated( __METHOD__, '1.32' );
return '';
}
* @author Tim Starling
*/
protected function driftHash( $data ) {
+ wfDeprecated( __METHOD__, '1.32' );
return '';
}
* @return string A new weak random state
*/
protected function randomState() {
+ wfDeprecated( __METHOD__, '1.32' );
return '';
}
* @return bool Always true
*/
public function wasStrong() {
+ wfDeprecated( __METHOD__, '1.32' );
return true;
}
* @return string Raw binary random data
*/
public function generate( $bytes ) {
+ wfDeprecated( __METHOD__, '1.32' );
$bytes = floor( $bytes );
return random_bytes( $bytes );
}
* @return string Hexadecimal random data
*/
public function generateHex( $chars ) {
+ wfDeprecated( __METHOD__, '1.32' );
return MWCryptRand::generateHex( $chars );
}
}
* Returns minified JavaScript code.
*
* @param string $s JavaScript code to minify
- * @return String Minified code
+ * @return string Minified code
*/
public static function minify( $s ) {
// First we declare a few tables that contain our parsing rules
$xml = new XmlTypeCheck( $file );
if ( $xml->wellFormed ) {
$xmlTypes = $this->xmlTypes;
- if ( isset( $xmlTypes[$xml->getRootElement()] ) ) {
- return $xmlTypes[$xml->getRootElement()];
- } else {
- return 'application/xml';
- }
+ return $xmlTypes[$xml->getRootElement()] ?? 'application/xml';
}
/**
* @param array $params
*/
public function __construct( array $params = [] ) {
- if ( isset( $params['logger'] ) ) {
- $this->setLogger( $params['logger'] );
- } else {
- $this->setLogger( new NullLogger() );
- }
+ $this->setLogger( $params['logger'] ?? new NullLogger() );
if ( isset( $params['keyspace'] ) ) {
$this->keyspace = $params['keyspace'];
$this->serverTagMap[is_int( $key ) ? $server : $key] = $server;
}
- if ( isset( $params['automaticFailover'] ) ) {
- $this->automaticFailover = $params['automaticFailover'];
- } else {
- $this->automaticFailover = true;
- }
+ $this->automaticFailover = $params['automaticFailover'] ?? true;
$this->attrMap[self::ATTR_SYNCWRITES] = self::QOS_SYNCWRITES_NONE;
}
switch ( $type ) {
case self::ESTIMATE_DB_APPLY:
- $this->ping( $rtt );
- $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
- $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
- // For omitted queries, make them count as something at least
- $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
- $applyTime += self::TINY_WRITE_SEC * $omitted;
-
- return $applyTime;
+ return $this->pingAndCalculateLastTrxApplyTime();
default: // everything
return $this->trxWriteDuration;
}
}
+ /**
+ * @return float Time to apply writes to replicas based on trxWrite* fields
+ */
+ private function pingAndCalculateLastTrxApplyTime() {
+ $this->ping( $rtt );
+
+ $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
+ $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
+ // For omitted queries, make them count as something at least
+ $omitted = $this->trxWriteQueryCount - $this->trxWriteAdjQueryCount;
+ $applyTime += self::TINY_WRITE_SEC * $omitted;
+
+ return $applyTime;
+ }
+
public function pendingWriteCallers() {
return $this->trxLevel ? $this->trxWriteCallers : [];
}
// Avoid fatals if close() was called
$this->assertOpen();
- $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
$this->doRollback( $fname );
$this->trxStatus = self::STATUS_TRX_NONE;
$this->trxAtomicLevels = [];
+ // Estimate the RTT via a query now that trxStatus is OK
+ $writeTime = $this->pingAndCalculateLastTrxApplyTime();
if ( $this->trxDoneWrites ) {
$this->trxProfiler->transactionWritingOut(
*/
public function getServerVersion() {
$server_info = sqlsrv_server_info( $this->conn );
- $version = 'Error';
- if ( isset( $server_info['SQLServerVersion'] ) ) {
- $version = $server_info['SQLServerVersion'];
- }
+ $version = $server_info['SQLServerVersion'] ?? 'Error';
return $version;
}
return $this->lastSection;
}
list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
- if ( isset( $this->sectionsByDB[$dbName] ) ) {
- $section = $this->sectionsByDB[$dbName];
- } else {
- $section = 'DEFAULT';
- }
+ $section = $this->sectionsByDB[$dbName] ?? 'DEFAULT';
$this->lastSection = $section;
$this->lastDomain = $domain;
public function newMainLB( $domain = false ) {
list( $dbName, ) = $this->getDBNameAndPrefix( $domain );
$section = $this->getSectionForDomain( $domain );
- if ( isset( $this->groupLoadsByDB[$dbName] ) ) {
- $groupLoads = $this->groupLoadsByDB[$dbName];
- } else {
- $groupLoads = [];
- }
+ $groupLoads = $this->groupLoadsByDB[$dbName] ?? [];
if ( isset( $this->groupLoadsBySection[$section] ) ) {
$groupLoads = array_merge_recursive(
if ( isset( $groupLoadsByServer[$serverName] ) ) {
$serverInfo['groupLoads'] = $groupLoadsByServer[$serverName];
}
- if ( isset( $this->hostsByName[$serverName] ) ) {
- $serverInfo['host'] = $this->hostsByName[$serverName];
- } else {
- $serverInfo['host'] = $serverName;
- }
+ $serverInfo['host'] = $this->hostsByName[$serverName] ?? $serverName;
$serverInfo['hostName'] = $serverName;
$serverInfo['load'] = $load;
$serverInfo += [ 'flags' => IDatabase::DBO_DEFAULT ];
$this->maxLag = $params['maxLag'];
}
- if ( isset( $params['loadMonitor'] ) ) {
- $this->loadMonitorConfig = $params['loadMonitor'];
- } else {
- $this->loadMonitorConfig = [ 'class' => 'LoadMonitorNull' ];
- }
+ $this->loadMonitorConfig = $params['loadMonitor'] ?? [ 'class' => 'LoadMonitorNull' ];
$this->loadMonitorConfig += [ 'lagWarnThreshold' => $this->maxLag ];
foreach ( $params['servers'] as $i => $server ) {
}
}
- if ( isset( $params['srvCache'] ) ) {
- $this->srvCache = $params['srvCache'];
- } else {
- $this->srvCache = new EmptyBagOStuff();
- }
- if ( isset( $params['wanCache'] ) ) {
- $this->wanCache = $params['wanCache'];
- } else {
- $this->wanCache = WANObjectCache::newEmpty();
- }
+ $this->srvCache = $params['srvCache'] ?? new EmptyBagOStuff();
+ $this->wanCache = $params['wanCache'] ?? WANObjectCache::newEmpty();
$this->profiler = $params['profiler'] ?? null;
- if ( isset( $params['trxProfiler'] ) ) {
- $this->trxProfiler = $params['trxProfiler'];
- } else {
- $this->trxProfiler = new TransactionProfiler();
- }
+ $this->trxProfiler = $params['trxProfiler'] ?? new TransactionProfiler();
$this->errorLogger = $params['errorLogger'] ?? function ( Exception $e ) {
trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_USER_WARNING );
}
public function getServerName( $i ) {
- if ( isset( $this->servers[$i]['hostName'] ) ) {
- $name = $this->servers[$i]['hostName'];
- } elseif ( isset( $this->servers[$i]['host'] ) ) {
- $name = $this->servers[$i]['host'];
- } else {
- $name = '';
- }
+ $name = $this->servers[$i]['hostName'] ?? $this->servers[$i]['host'] ?? '';
return ( $name != '' ) ? $name : 'localhost';
}
public function getServerInfo( $i ) {
- if ( isset( $this->servers[$i] ) ) {
- return $this->servers[$i];
- } else {
- return false;
- }
+ return $this->servers[$i] ?? false;
}
public function getServerType( $i ) {
* * $title is optional
* * $revision is optional
*
- * There are also deprecated "v1" requests; see onParsoid1Request
- * for details.
* @param array $params Key/value map
* - url : Parsoid server URL
* - domain : Wiki domain to use
# Map RESTBase v1 API to Parsoid v3 API (pretty easy)
$req['url'] = preg_replace( '#^local/v1/#', 'local/v3/', $req['url'] );
} elseif ( $version !== 'v3' ) {
- $result[$key] = $this->onParsoid1Request( $req, $idGeneratorFunc );
- continue;
+ throw new Exception( "Only Parsoid v3 API is supported." );
}
if ( $targetWiki !== 'local' ) {
throw new Exception( "Only 'local' target wiki is currently supported" );
return $result;
}
- /**
- * Remap a Parsoid v1 request to a Parsoid v3 request.
- *
- * Example Parsoid v1 requests:
- * GET /local/v1/page/$title/html/$oldid
- * * $oldid is optional
- * POST /local/v1/transform/html/to/wikitext/$title/$oldid
- * * body: array( 'html' => ... )
- * * $title and $oldid are optional
- * POST /local/v1/transform/wikitext/to/html/$title
- * * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'body' => true/false )
- * * $title is optional
- *
- * NOTE: the POST APIs aren't "real" Parsoid v1 APIs, they are just what
- * Visual Editor "pretends" the V1 API is like. A previous version of
- * ParsoidVirtualRESTService translated these to the "real" Parsoid v1
- * API. We now translate these to the "real" Parsoid v3 API.
- * @param array $req
- * @param Closure $idGeneratorFunc
- * @return array
- * @throws Exception
- * @deprecated since 1.26, upgrade your client to issue v3 requests.
- */
- public function onParsoid1Request( array $req, Closure $idGeneratorFunc ) {
- wfDeprecated( __METHOD__, '1.26' );
- $parts = explode( '/', $req['url'] );
- list(
- $targetWiki, // 'local'
- $version, // 'v1'
- $reqType // 'page' or 'transform'
- ) = $parts;
- if ( $targetWiki !== 'local' ) {
- throw new Exception( "Only 'local' target wiki is currently supported" );
- } elseif ( $version !== 'v1' ) {
- throw new Exception( "Only v1 and v3 are supported." );
- } elseif ( $reqType !== 'page' && $reqType !== 'transform' ) {
- throw new Exception( "Request type must be either 'page' or 'transform'" );
- }
- $req['url'] = $this->params['url'] . $this->params['domain'] . '/v3/';
- if ( $reqType === 'page' ) {
- $title = $parts[3];
- if ( $parts[4] !== 'html' ) {
- throw new Exception( "Only 'html' output format is currently supported" );
- }
- $req['url'] .= 'page/html/' . $title;
- if ( isset( $parts[5] ) ) {
- $req['url'] .= '/' . $parts[5];
- } elseif ( isset( $req['query']['oldid'] ) && $req['query']['oldid'] ) {
- $req['url'] .= '/' . $req['query']['oldid'];
- unset( $req['query']['oldid'] );
- }
- } elseif ( $reqType === 'transform' ) {
- $req['url'] .= 'transform/' . $parts[3] . '/to/' . $parts[5];
- // the title
- if ( isset( $parts[6] ) ) {
- $req['url'] .= '/' . $parts[6];
- }
- // revision id
- if ( isset( $parts[7] ) ) {
- $req['url'] .= '/' . $parts[7];
- } elseif ( isset( $req['body']['oldid'] ) && $req['body']['oldid'] ) {
- $req['url'] .= '/' . $req['body']['oldid'];
- unset( $req['body']['oldid'] );
- }
- if ( $parts[4] !== 'to' ) {
- throw new Exception( "Part index 4 is not 'to'" );
- }
- if ( $parts[3] === 'html' && $parts[5] === 'wikitext' ) {
- if ( !isset( $req['body']['html'] ) ) {
- throw new Exception( "You must set an 'html' body key for this request" );
- }
- } elseif ( $parts[3] == 'wikitext' && $parts[5] == 'html' ) {
- if ( !isset( $req['body']['wikitext'] ) ) {
- throw new Exception( "You must set a 'wikitext' body key for this request" );
- }
- if ( isset( $req['body']['body'] ) ) {
- $req['body']['body_only'] = $req['body']['body'];
- unset( $req['body']['body'] );
- }
- } else {
- throw new Exception( "Transformation unsupported" );
- }
- }
- // set the appropriate proxy, timeout and headers
- if ( $this->params['HTTPProxy'] ) {
- $req['proxy'] = $this->params['HTTPProxy'];
- }
- if ( $this->params['timeout'] != null ) {
- $req['reqTimeout'] = $this->params['timeout'];
- }
- if ( $this->params['forwardCookies'] ) {
- $req['headers']['Cookie'] = $this->params['forwardCookies'];
- }
-
- return $req;
- }
-
}
}
/**
- * Remaps Parsoid v1/v3 requests to RESTBase v1 requests.
+ * Remaps Parsoid v3 requests to RESTBase v1 requests.
* @param array $reqs
* @param Closure $idGeneratorFunc
* @return array
$version = explode( '/', $req['url'] )[1];
if ( $version === 'v3' ) {
$result[$key] = $this->onParsoid3Request( $req, $idGeneratorFunc );
- } elseif ( $version === 'v1' ) {
- $result[$key] = $this->onParsoid1Request( $req, $idGeneratorFunc );
} else {
- throw new Exception( "Only v1 and v3 are supported." );
+ throw new Exception( "Only Parsoid v3 is supported." );
}
}
return $result;
}
- /**
- * Remap a Parsoid v1 request to a RESTBase v1 request.
- *
- * Example Parsoid v1 requests:
- * GET /local/v1/page/$title/html/$oldid
- * * $oldid is optional
- * POST /local/v1/transform/html/to/wikitext/$title/$oldid
- * * body: array( 'html' => ... )
- * * $title and $oldid are optional
- * POST /local/v1/transform/wikitext/to/html/$title
- * * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'body' => true/false )
- * * $title is optional
- *
- * NOTE: the POST APIs aren't "real" Parsoid v1 APIs, they are just what
- * Visual Editor "pretends" the V1 API is like. (See
- * ParsoidVirtualRESTService.)
- * @param array $req
- * @param Closure $idGeneratorFunc
- * @return array
- * @throws Exception
- * @deprecated since 1.26, upgrade your client to issue v3 requests.
- */
- public function onParsoid1Request( array $req, Closure $idGeneratorFunc ) {
- wfDeprecated( __METHOD__, '1.26' );
- $parts = explode( '/', $req['url'] );
- list(
- $targetWiki, // 'local'
- $version, // 'v1'
- $reqType // 'page' or 'transform'
- ) = $parts;
- if ( $targetWiki !== 'local' ) {
- throw new Exception( "Only 'local' target wiki is currently supported" );
- } elseif ( $version !== 'v1' ) {
- throw new Exception( "Version mismatch: should not happen." );
- } elseif ( $reqType !== 'page' && $reqType !== 'transform' ) {
- throw new Exception( "Request type must be either 'page' or 'transform'" );
- }
- $req['url'] = $this->params['url'] . $this->params['domain'] . '/v1/' . $reqType . '/';
- if ( $reqType === 'page' ) {
- $title = $parts[3];
- if ( $parts[4] !== 'html' ) {
- throw new Exception( "Only 'html' output format is currently supported" );
- }
- $req['url'] .= 'html/' . $title;
- if ( isset( $parts[5] ) ) {
- $req['url'] .= '/' . $parts[5];
- } elseif ( isset( $req['query']['oldid'] ) && $req['query']['oldid'] ) {
- $req['url'] .= '/' . $req['query']['oldid'];
- unset( $req['query']['oldid'] );
- }
- } elseif ( $reqType === 'transform' ) {
- // from / to transform
- $req['url'] .= $parts[3] . '/to/' . $parts[5];
- // the title
- if ( isset( $parts[6] ) ) {
- $req['url'] .= '/' . $parts[6];
- }
- // revision id
- if ( isset( $parts[7] ) ) {
- $req['url'] .= '/' . $parts[7];
- } elseif ( isset( $req['body']['oldid'] ) && $req['body']['oldid'] ) {
- $req['url'] .= '/' . $req['body']['oldid'];
- unset( $req['body']['oldid'] );
- }
- if ( $parts[4] !== 'to' ) {
- throw new Exception( "Part index 4 is not 'to'" );
- }
- if ( $parts[3] === 'html' && $parts[5] === 'wikitext' ) {
- if ( !isset( $req['body']['html'] ) ) {
- throw new Exception( "You must set an 'html' body key for this request" );
- }
- } elseif ( $parts[3] == 'wikitext' && $parts[5] == 'html' ) {
- if ( !isset( $req['body']['wikitext'] ) ) {
- throw new Exception( "You must set a 'wikitext' body key for this request" );
- }
- if ( isset( $req['body']['body'] ) ) {
- $req['body']['body_only'] = $req['body']['body'];
- unset( $req['body']['body'] );
- }
- } else {
- throw new Exception( "Transformation unsupported" );
- }
- }
- // set the appropriate proxy, timeout and headers
- if ( $this->params['HTTPProxy'] ) {
- $req['proxy'] = $this->params['HTTPProxy'];
- }
- if ( $this->params['timeout'] != null ) {
- $req['reqTimeout'] = $this->params['timeout'];
- }
- if ( $this->params['forwardCookies'] ) {
- $req['headers']['Cookie'] = $this->params['forwardCookies'];
- }
-
- return $req;
- }
-
/**
* Remap a Parsoid v3 request to a RESTBase v1 request.
*
);
$params[5] = isset( $params[5] ) ?
self::formatBlockFlags( $params[5], $this->context->getLanguage() ) : '';
+
+ // block restrictions
+ if ( isset( $params[6] ) ) {
+ $pages = $params[6]['pages'] ?? [];
+ $pages = array_map( function ( $page ){
+ return $this->makePageLink( Title::newFromText( ( $page ) ) );
+ }, $pages );
+
+ $params[6] = Message::rawParam( $this->context->getLanguage()->listToText( $pages ) );
+ $params[7] = count( $pages );
+ }
}
return $params;
'6:array:flags',
'6::flags' => '6:array:flags',
];
+
foreach ( $map as $index => $key ) {
if ( isset( $params[$index] ) ) {
$params[$key] = $params[$index];
}
}
+ ksort( $params );
+
$subtype = $entry->getSubtype();
if ( $subtype === 'block' || $subtype === 'reblock' ) {
// Defaults for old log entries missing some fields
if ( isset( $ret['flags'] ) ) {
ApiResult::setIndexedTagName( $ret['flags'], 'f' );
}
+
+ if ( isset( $ret['restrictions']['pages'] ) ) {
+ $ret['restrictions']['pages'] = array_map( function ( $title ) {
+ return $this->formatParameterValueForApi( 'page', 'title-link', $title );
+ }, $ret['restrictions']['pages'] );
+ ApiResult::setIndexedTagName( $ret['restrictions']['pages'], 'p' );
+ }
+
return $ret;
}
+ protected function getMessageKey() {
+ $type = $this->entry->getType();
+ $subtype = $this->entry->getSubtype();
+ $sitewide = $this->entry->getParameters()['sitewide'] ?? true;
+
+ $key = "logentry-$type-$subtype";
+ if ( ( $subtype === 'block' || $subtype === 'reblock' ) && !$sitewide ) {
+ // $this->getMessageParameters is doing too much. We just need
+ // to check the presence of restrictions ($param[6]) and calling
+ // on parent gives us that
+ $params = parent::getMessageParameters();
+
+ // message changes depending on whether there are editing restrictions or not
+ if ( isset( $params[6] ) ) {
+ $key = "logentry-partial$type-$subtype";
+ } else {
+ $key = "logentry-non-editing-$type-$subtype";
+ }
+ }
+
+ return $key;
+ }
}
global $wgLogActionsHandlers;
$fulltype = $entry->getFullType();
$wildcard = $entry->getType() . '/*';
- $handler = '';
-
- if ( isset( $wgLogActionsHandlers[$fulltype] ) ) {
- $handler = $wgLogActionsHandlers[$fulltype];
- } elseif ( isset( $wgLogActionsHandlers[$wildcard] ) ) {
- $handler = $wgLogActionsHandlers[$wildcard];
- }
+ $handler = $wgLogActionsHandlers[$fulltype] ?? $wgLogActionsHandlers[$wildcard] ?? '';
if ( $handler !== '' && is_string( $handler ) && class_exists( $handler ) ) {
return new $handler( $entry );
global $wgLogNames;
// BC
- if ( isset( $wgLogNames[$this->type] ) ) {
- $key = $wgLogNames[$this->type];
- } else {
- $key = 'log-name-' . $this->type;
- }
+ $key = $wgLogNames[$this->type] ?? 'log-name-' . $this->type;
return wfMessage( $key );
}
public function getDescription() {
global $wgLogHeaders;
// BC
- if ( isset( $wgLogHeaders[$this->type] ) ) {
- $key = $wgLogHeaders[$this->type];
- } else {
- $key = 'log-description-' . $this->type;
- }
+ $key = $wgLogHeaders[$this->type] ?? 'log-description-' . $this->type;
return wfMessage( $key );
}
*/
public function getRestriction() {
global $wgLogRestrictions;
- if ( isset( $wgLogRestrictions[$this->type] ) ) {
- $restriction = $wgLogRestrictions[$this->type];
- } else {
- // '' always returns true with $user->isAllowed()
- $restriction = '';
- }
-
- return $restriction;
+ // '' always returns true with $user->isAllowed()
+ return $wgLogRestrictions[$this->type] ?? '';
}
/**
* @param string|null $name Human-readable name if a string address is given
* @param string|null $realName Human-readable real name if a string address is given
*/
- function __construct( $address, $name = null, $realName = null ) {
+ public function __construct( $address, $name = null, $realName = null ) {
$this->address = strval( $address );
$this->name = strval( $name );
$this->realName = strval( $realName );
* @return string
*/
function toString() {
+ if ( !$this->address ) {
+ return '';
+ }
+
# PHP's mail() implementation under Windows is somewhat shite, and
# can't handle "Joe Bloggs <joe@bloggs.com>" format email addresses,
# so don't bother generating them
- if ( $this->address ) {
- if ( $this->name != '' && !wfIsWindows() ) {
- global $wgEnotifUseRealName;
- $name = ( $wgEnotifUseRealName && $this->realName !== '' ) ? $this->realName : $this->name;
- $quoted = UserMailer::quotedPrintable( $name );
- // Must only be quoted if string does not use =? encoding (T191931)
- if ( $quoted === $name ) {
- $quoted = '"' . addslashes( $quoted ) . '"';
- }
- return "$quoted <{$this->address}>";
- } else {
- return $this->address;
- }
- } else {
- return "";
+ if ( $this->name === '' || wfIsWindows() ) {
+ return $this->address;
}
+
+ global $wgEnotifUseRealName;
+ $name = ( $wgEnotifUseRealName && $this->realName !== '' ) ? $this->realName : $this->name;
+ $quoted = UserMailer::quotedPrintable( $name );
+ // Must only be quoted if string does not use =? encoding (T191931)
+ if ( $quoted === $name ) {
+ $quoted = '"' . addslashes( $quoted ) . '"';
+ }
+
+ return "$quoted <{$this->address}>";
}
function __toString() {
case '2#055':
// Date created (not date digitized).
// Maps to exif DateTimeOriginal
- if ( isset( $parsed['2#060'] ) ) {
- $time = $parsed['2#060'];
- } else {
- $time = [];
- }
+ $time = $parsed['2#060'] ?? [];
$timestamp = self::timeHelper( $val, $time, $c );
if ( $timestamp ) {
$data['DateTimeOriginal'] = $timestamp;
case '2#062':
// Date converted to digital representation.
// Maps to exif DateTimeDigitized
- if ( isset( $parsed['2#063'] ) ) {
- $time = $parsed['2#063'];
- } else {
- $time = [];
- }
+ $time = $parsed['2#063'] ?? [];
$timestamp = self::timeHelper( $val, $time, $c );
if ( $timestamp ) {
$data['DateTimeDigitized'] = $timestamp;
case '2#030':
// Date released.
- if ( isset( $parsed['2#035'] ) ) {
- $time = $parsed['2#035'];
- } else {
- $time = [];
- }
+ $time = $parsed['2#035'] ?? [];
$timestamp = self::timeHelper( $val, $time, $c );
if ( $timestamp ) {
$data['DateTimeReleased'] = $timestamp;
case '2#037':
// Date expires.
- if ( isset( $parsed['2#038'] ) ) {
- $time = $parsed['2#038'];
- } else {
- $time = [];
- }
+ $time = $parsed['2#038'] ?? [];
$timestamp = self::timeHelper( $val, $time, $c );
if ( $timestamp ) {
$data['DateTimeExpires'] = $timestamp;
/**
* Converts a dimensions array about a potentially multipage document from an
* exhaustive list of ordered page numbers to a list of page ranges
- * @param Array $pagesByDimensions
- * @return String
+ * @param array $pagesByDimensions
+ * @return string
* @since 1.30
*/
public static function getPageRangesByDimensions( $pagesByDimensions ) {
* @throws InvalidArgumentException
*/
public static function newFromParams( $params ) {
- if ( isset( $params['loggroup'] ) ) {
- $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] );
- } else {
- $params['logger'] = LoggerFactory::getInstance( 'objectcache' );
- }
+ $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] ?? 'objectcache' );
if ( !isset( $params['keyspace'] ) ) {
$params['keyspace'] = self::getDefaultKeyspace();
}
}
}
$params['cache'] = self::newFromParams( $params['store'] );
- if ( isset( $params['loggroup'] ) ) {
- $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] );
- } else {
- $params['logger'] = LoggerFactory::getInstance( 'objectcache' );
- }
+ $params['logger'] = LoggerFactory::getInstance( $params['loggroup'] ?? 'objectcache' );
if ( !$wgCommandLineMode ) {
// Send the statsd data post-send on HTTP requests; avoid in CLI mode (T181385)
$params['stats'] = $services->getStatsdDataFactory();
// Note that the ArticleViewHeader hook is allowed to set $outputDone to a
// ParserOutput instance.
$pOutput = ( $outputDone instanceof ParserOutput )
- // phpcs:ignore MediaWiki.Usage.NestedInlineTernary.UnparenthesizedTernary -- FIXME T203805
? $outputDone // object fetched by hook
: ( $this->mParserOutput ?: null ); // ParserOutput or null, avoid false
$dbw->startAtomic( __METHOD__ );
if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) {
+ $contLang = MediaWikiServices::getInstance()->getContentLanguage();
+ $truncatedFragment = $contLang->truncateForDatabase( $rt->getFragment(), 255 );
$dbw->upsert(
'redirect',
[
'rd_from' => $this->getId(),
'rd_namespace' => $rt->getNamespace(),
'rd_title' => $rt->getDBkey(),
- 'rd_fragment' => $rt->getFragment(),
+ 'rd_fragment' => $truncatedFragment,
'rd_interwiki' => $rt->getInterwiki(),
],
[ 'rd_from' ],
[
'rd_namespace' => $rt->getNamespace(),
'rd_title' => $rt->getDBkey(),
- 'rd_fragment' => $rt->getFragment(),
+ 'rd_fragment' => $truncatedFragment,
'rd_interwiki' => $rt->getInterwiki(),
],
__METHOD__
// in the job queue to avoid simultaneous deletion operations would add overhead.
// Number of archived revisions cannot be known beforehand, because edits can be made
// while deletion operations are being processed, changing the number of archivals.
- $archivedRevisionCount = $dbw->selectRowCount(
- 'archive', '1', [ 'ar_page_id' => $id ], __METHOD__
+ $archivedRevisionCount = $dbw->selectField(
+ 'archive', 'COUNT(*)',
+ [
+ 'ar_namespace' => $this->getTitle()->getNamespace(),
+ 'ar_title' => $this->getTitle()->getDBkey(),
+ 'ar_page_id' => $id
+ ], __METHOD__
);
// Clone the title and wikiPage, so we have the information we need when
*/
private function replace( $matches ) {
# Extract information from $matches
- $linked = true;
- if ( isset( $this->mLinked ) ) {
- $linked = $this->mLinked;
- }
+ $linked = $this->mLinked ?? true;
$bits = [];
$key = $this->keys[$this->mSource];
* or null if no value was set for this key.
*/
public function getExtensionData( $key ) {
- if ( isset( $this->mExtensionData[$key] ) ) {
- return $this->mExtensionData[$key];
- }
-
- return null;
+ return $this->mExtensionData[$key] ?? null;
}
private static function getTimes( $clock = null ) {
* @return string|bool
*/
public function getArgument( $index ) {
- if ( !isset( $this->args[$index] ) ) {
- return false;
- }
- return $this->args[$index];
+ return $this->args[$index] ?? false;
}
public function getArguments() {
* @return string|bool
*/
public function getArgument( $index ) {
- if ( !isset( $this->args[$index] ) ) {
- return false;
- }
- return $this->args[$index];
+ return $this->args[$index] ?? false;
}
public function getArguments() {
continue;
}
- if ( isset( $userGroupMemberships[$ueg] ) ) {
- $groupStringOrObject = $userGroupMemberships[$ueg];
- } else {
- $groupStringOrObject = $ueg;
- }
+ $groupStringOrObject = $userGroupMemberships[$ueg] ?? $ueg;
$userG = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html' );
$userM = UserGroupMembership::getLink( $groupStringOrObject, $context, 'html',
'section' => 'editing/editor',
'label-message' => 'tog-useeditwarning',
];
- $defaultPreferences['showtoolbar'] = [
- 'type' => 'toggle',
- 'section' => 'editing/editor',
- 'label-message' => 'tog-showtoolbar',
- ];
$defaultPreferences['previewonfirst'] = [
'type' => 'toggle',
* @param string $dir
*/
protected function extractConfig2( array $info, $dir ) {
- if ( isset( $info['config_prefix'] ) ) {
- $prefix = $info['config_prefix'];
- } else {
- $prefix = 'wg';
- }
+ $prefix = $info['config_prefix'] ?? 'wg';
if ( isset( $info['config'] ) ) {
foreach ( $info['config'] as $key => $data ) {
$value = $data['value'];
$object->setConfig( $this->getConfig() );
$object->setLogger( $this->logger );
} else {
- if ( !isset( $info['class'] ) ) {
- $class = ResourceLoaderFileModule::class;
- } else {
- $class = $info['class'];
- }
+ $class = $info['class'] ?? ResourceLoaderFileModule::class;
/** @var ResourceLoaderModule $object */
$object = new $class( $info );
$object->setConfig( $this->getConfig() );
* - new XmlJsCode( '{}' )
* - new stdClass() // (object) []
*
- * @param Array $array
+ * @param array $array
*/
private static function trimArray( array &$array ) {
$i = count( $array );
+++ /dev/null
-<?php
-/**
- * ResourceLoader module for the edit toolbar.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- */
-
-/**
- * ResourceLoader module for the edit toolbar.
- *
- * @since 1.24
- */
-class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
- /**
- * Get language-specific LESS variables for this module.
- *
- * @since 1.27
- * @param ResourceLoaderContext $context
- * @return array
- */
- protected function getLessVars( ResourceLoaderContext $context ) {
- $vars = parent::getLessVars( $context );
- $language = Language::factory( $context->getLanguage() );
- foreach ( $language->getImageFiles() as $key => $value ) {
- $vars[$key] = CSSMin::serializeStringValue( $value );
- }
- return $vars;
- }
-}
* @return mixed the feature value or null if unset
*/
public function getFeatureData( $feature ) {
- if ( isset( $this->features[$feature] ) ) {
- return $this->features[$feature];
- }
- return null;
+ return $this->features[$feature] ?? null;
}
/**
// Give site config file a chance to run the script in a wrapper.
// The caller may likely want to call wfBasename() on $script.
Hooks::run( 'wfShellWikiCmd', [ &$script, &$parameters, &$options ] );
- $cmd = isset( $options['php'] ) ? [ $options['php'] ] : [ $wgPhpCli ];
+ $cmd = [ $options['php'] ?? $wgPhpCli ];
if ( isset( $options['wrapper'] ) ) {
$cmd[] = $options['wrapper'];
}
* @return Site|null
*/
public function getSite( $globalId, $source = 'cache' ) {
- if ( isset( $this->sites[$globalId] ) ) {
- return $this->sites[$globalId];
- } else {
- return null;
- }
+ return $this->sites[$globalId] ?? null;
}
/**
* @return string
*/
function makeLink( $key, $item, $options = [] ) {
- if ( isset( $item['text'] ) ) {
- $text = $item['text'];
- } else {
- $text = wfMessage( $item['msg'] ?? $key )->text();
- }
+ $text = $item['text'] ?? wfMessage( $item['msg'] ?? $key )->text();
$html = htmlspecialchars( $text );
* @return Title
*/
public function getRelevantTitle() {
- if ( isset( $this->mRelevantTitle ) ) {
- return $this->mRelevantTitle;
- }
- return $this->getTitle();
+ return $this->mRelevantTitle ?? $this->getTitle();
}
/**
/**
* Fetch the change tags list for the front end
*
- * @return Array Tag data
+ * @return array Tag data
*/
protected function getChangeTagList() {
$cache = ObjectCache::getMainWANInstance();
$caseFoldedAlias = $this->contLang->caseFold( $bits[0] );
$caseFoldedAlias = str_replace( ' ', '_', $caseFoldedAlias );
$aliases = $this->getAliasList();
- if ( isset( $aliases[$caseFoldedAlias] ) ) {
- $name = $aliases[$caseFoldedAlias];
- } else {
+ if ( !isset( $aliases[$caseFoldedAlias] ) ) {
return [ null, null ];
}
-
- if ( !isset( $bits[1] ) ) { // T4087
- $par = null;
- } else {
- $par = $bits[1];
- }
+ $name = $aliases[$caseFoldedAlias];
+ $par = $bits[1] ?? null; // T4087
return [ $name, $par ];
}
// @todo FIXME: Redirects broken due to this call
$bits = explode( '/', $title->getDBkey(), 2 );
$name = $bits[0];
- if ( !isset( $bits[1] ) ) { // T4087
- $par = null;
- } else {
- $par = $bits[1];
- }
+ $par = $bits[1] ?? null; // T4087
$page = $this->getPage( $name );
if ( !$page ) {
* @ingroup SpecialPage
*/
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+
/**
* A special page that allows users with 'block' right to block users from
* editing pages and other actions
$conf = $this->getConfig();
$oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
+ $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
- $a = [
- 'Target' => [
- 'type' => 'user',
- 'ipallowed' => true,
- 'iprange' => true,
- 'label-message' => 'ipaddressorusername',
- 'id' => 'mw-bi-target',
- 'size' => '45',
- 'autofocus' => true,
- 'required' => true,
- 'validation-callback' => [ __CLASS__, 'validateTargetField' ],
- ],
- 'Expiry' => [
- 'type' => 'expiry',
- 'label-message' => 'ipbexpiry',
- 'required' => true,
- 'options' => $suggestedDurations,
- 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
- ],
- 'Reason' => [
- 'type' => 'selectandother',
- // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
- // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
- // Unicode codepoints (or 255 UTF-8 bytes for old schema).
- 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
- 'maxlength-unit' => 'codepoints',
- 'label-message' => 'ipbreason',
- 'options-message' => 'ipbreason-dropdown',
- ],
- 'CreateAccount' => [
- 'type' => 'check',
- 'label-message' => 'ipbcreateaccount',
- 'default' => true,
- ],
+ $a = [];
+
+ $a['Target'] = [
+ 'type' => 'user',
+ 'ipallowed' => true,
+ 'iprange' => true,
+ 'label-message' => 'ipaddressorusername',
+ 'id' => 'mw-bi-target',
+ 'size' => '45',
+ 'autofocus' => true,
+ 'required' => true,
+ 'validation-callback' => [ __CLASS__, 'validateTargetField' ],
+ ];
+
+ if ( $enablePartialBlocks ) {
+ $a['EditingRestriction'] = [
+ 'type' => 'radio',
+ 'label' => $this->msg( 'ipb-type-label' )->text(),
+ 'options' => [
+ $this->msg( 'ipb-sitewide' )->text() => 'sitewide',
+ $this->msg( 'ipb-partial' )->text() => 'partial',
+ ],
+ ];
+ $a['PageRestrictions'] = [
+ 'type' => 'titlesmultiselect',
+ 'label' => $this->msg( 'ipb-pages-label' )->text(),
+ 'exists' => true,
+ 'max' => 10,
+ 'cssclass' => 'mw-block-page-restrictions',
+ ];
+ }
+
+ $a['Expiry'] = [
+ 'type' => 'expiry',
+ 'label-message' => 'ipbexpiry',
+ 'required' => true,
+ 'options' => $suggestedDurations,
+ 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
+ ];
+
+ $a['Reason'] = [
+ 'type' => 'selectandother',
+ // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
+ // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
+ // Unicode codepoints (or 255 UTF-8 bytes for old schema).
+ 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
+ 'maxlength-unit' => 'codepoints',
+ 'label-message' => 'ipbreason',
+ 'options-message' => 'ipbreason-dropdown',
+ ];
+
+ $a['CreateAccount'] = [
+ 'type' => 'check',
+ 'label-message' => 'ipbcreateaccount',
+ 'default' => true,
];
if ( self::canBlockEmail( $user ) ) {
unset( $fields['Confirm']['default'] );
$this->preErrors[] = [ 'ipb-blockingself', 'ipb-confirmaction' ];
}
+
+ if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
+ if ( $block instanceof Block && !$block->isSitewide() ) {
+ $fields['EditingRestriction']['default'] = 'partial';
+ } else {
+ $fields['EditingRestriction']['default'] = 'sitewide';
+ }
+
+ if ( $block instanceof Block ) {
+ $pageRestrictions = [];
+ foreach ( $block->getRestrictions() as $restriction ) {
+ if ( $restriction->getType() !== 'page' ) {
+ continue;
+ }
+
+ $pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
+ }
+
+ // Sort the restrictions so they are in alphabetical order.
+ sort( $pageRestrictions );
+ $fields['PageRestrictions']['default'] = implode( "\n", $pageRestrictions );
+ }
+ }
}
/**
global $wgBlockAllowsUTEdit, $wgHideUserContribLimit;
$performer = $context->getUser();
+ $enablePartialBlocks = $context->getConfig()->get( 'EnablePartialBlocks' );
// Handled by field validator callback
// self::validateTargetField( $data['Target'] );
$block->isAutoblocking( $data['AutoBlock'] );
$block->mHideName = $data['HideUser'];
+ if (
+ $enablePartialBlocks &&
+ isset( $data['EditingRestriction'] ) &&
+ $data['EditingRestriction'] === 'partial'
+ ) {
+ $block->isSitewide( false );
+ }
+
$reason = [ 'hookaborted' ];
if ( !Hooks::run( 'BlockIp', [ &$block, &$performer, &$reason ] ) ) {
return $reason;
}
+ $restrictions = [];
+ if ( $enablePartialBlocks ) {
+ if ( !empty( $data['PageRestrictions'] ) ) {
+ $restrictions = array_map( function ( $text ) {
+ $title = Title::newFromText( $text );
+ // Use the link cache since the title has already been loaded when
+ // the field was validated.
+ $restriction = new PageRestriction( 0, $title->getArticleId() );
+ $restriction->setTitle( $title );
+ return $restriction;
+ }, explode( "\n", $data['PageRestrictions'] ) );
+ }
+
+ $block->setRestrictions( $restrictions );
+ }
+
$priorBlock = null;
# Try to insert block. Is there a conflicting block?
$status = $block->insert();
$currentBlock->prevents( 'editownusertalk', $block->prevents( 'editownusertalk' ) );
$currentBlock->mReason = $block->mReason;
+ if ( $enablePartialBlocks ) {
+ // Maintain the sitewide status. If partial blocks is not enabled,
+ // saving the block will result in a sitewide block.
+ $currentBlock->isSitewide( $block->isSitewide() );
+
+ // Set the block id of the restrictions.
+ $currentBlock->setRestrictions(
+ BlockRestriction::setBlockId( $currentBlock->getId(), $restrictions )
+ );
+ }
+
$status = $currentBlock->update();
$logaction = 'reblock';
$logParams = [];
$logParams['5::duration'] = $data['Expiry'];
$logParams['6::flags'] = self::blockLogFlags( $data, $type );
+ $logParams['sitewide'] = $block->isSitewide();
+
+ if ( $enablePartialBlocks && !empty( $data['PageRestrictions'] ) ) {
+ $logParams['7::restrictions'] = [
+ 'pages' => explode( "\n", $data['PageRestrictions'] ),
+ ];
+ }
# Make log entry, if the name is hidden, put it in the suppression log
$log_type = $data['HideUser'] ? 'suppress' : 'block';
* @return string
*/
protected static function blockLogFlags( array $data, $type ) {
- global $wgBlockAllowsUTEdit;
+ $config = RequestContext::getMain()->getConfig();
+
+ $blockAllowsUTEdit = $config->get( 'BlockAllowsUTEdit' );
+
$flags = [];
# when blocking a user the option 'anononly' is not available/has no effect
$flags[] = 'noemail';
}
- if ( $wgBlockAllowsUTEdit && $data['DisableUTEdit'] ) {
+ if ( $blockAllowsUTEdit && $data['DisableUTEdit'] ) {
// For grepping: message block-log-flags-nousertalk
$flags[] = 'nousertalk';
}
case 'badaccess':
throw new PermissionsError( 'sendemail' );
case 'blockedemailuser':
- throw new UserBlockedError( $this->getUser()->mBlock );
+ throw $this->getBlockedEmailError();
case 'actionthrottledtext':
throw new ThrottledError;
case 'mailnologin':
protected function getGroupName() {
return 'users';
}
+
+ /**
+ * Builds an error message based on the block params
+ *
+ * @return ErrorPageError
+ */
+ private function getBlockedEmailError() {
+ $block = $this->getUser()->mBlock;
+ $params = $block->getBlockErrorParams( $this->getContext() );
+
+ $msg = $block->isSitewide() ? 'blockedtext' : 'blocked-email-user';
+ return new ErrorPageError( 'blockedtitle', $msg, $params );
+ }
}
}
/**
- * @return String
+ * @return string
*/
protected function getGroupName() {
return 'redirects';
*
* It's important that img_media_type come first, otherwise the
* tables will be fragmented.
- * @return Array Fields to sort by
+ * @return array Fields to sort by
*/
function getOrderFields() {
return [ 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ];
/**
* @param float $decimal A decimal percentage (ie for 12.3%, this would be 0.123)
- * @return String The percentage formatted so that 3 significant digits are shown.
+ * @return string The percentage formatted so that 3 significant digits are shown.
*/
protected function makePercentPretty( $decimal ) {
$decimal *= 100;
/**
* Get (not output) the header row for the table
*
- * @return String the header row of the able
+ * @return string The header row of the table
*/
protected function getTableHeaderRow() {
$headers = [ 'mimetype', 'extensions', 'count', 'totalbytes' ];
}
# Check blocks
- if ( $user->isBlocked() ) {
+ if ( $user->isBlockedFromUpload() ) {
throw new UserBlockedError( $user->getBlock() );
}
* @param UserGroupMembership[] $usergroups Associative array of (group name as string =>
* UserGroupMembership object) for groups the user belongs to
* @param User $user
- * @return Array with 2 elements: the XHTML table element with checkxboes, and
+ * @return array Array with 2 elements: the XHTML table element with checkxboes, and
* whether any groups are changeable
*/
private function groupCheckboxes( $usergroups, $user ) {
$label = Html::rawElement( 'label', [ 'for' => $id ], $this->mLabel );
if ( !empty( $this->mParams['radio'] ) ) {
- if ( isset( $this->mParams['radio-id'] ) ) {
- $radioId = $this->mParams['radio-id'];
- } else {
+ $radioId = $this->mParams['radio-id'] ??
// Old way. For the benefit of extensions that do not define
// the 'radio-id' key.
- $radioId = 'wpSourceType' . $this->mParams['upload-type'];
- }
+ 'wpSourceType' . $this->mParams['upload-type'];
$attribs = [
'name' => 'wpSourceType',
/**
* @ingroup Pager
*/
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\Restriction;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IResultWrapper;
protected $conds;
protected $page;
+ /**
+ * Array of restrictions.
+ *
+ * @var Restriction[]
+ */
+ protected $restrictions = [];
+
/**
* @param SpecialPage $page
* @param array $conds
'blocklist-nousertalk',
'unblocklink',
'change-blocklink',
+ 'blocklist-editing',
+ 'blocklist-editing-sitewide',
];
foreach ( $keys as $key ) {
case 'ipb_params':
$properties = [];
+
+ if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
+ if ( $row->ipb_sitewide ) {
+ $properties[] = htmlspecialchars( $msg['blocklist-editing-sitewide'] );
+ }
+ }
+
+ if ( !$row->ipb_sitewide && $this->restrictions ) {
+ $list = $this->getRestrictionListHTML( $this->restrictions, $row );
+ $properties[] = htmlspecialchars( $msg['blocklist-editing'] ) . $list;
+ }
+
if ( $row->ipb_anon_only ) {
$properties[] = htmlspecialchars( $msg['anononlyblock'] );
}
$properties[] = htmlspecialchars( $msg['blocklist-nousertalk'] );
}
- $formatted = $language->commaList( $properties );
+ $formatted = Html::rawElement(
+ 'ul',
+ [],
+ implode( '', array_map( function ( $prop ) {
+ return HTML::rawElement(
+ 'li',
+ [],
+ $prop
+ );
+ }, $properties ) )
+ );
break;
default:
return $formatted;
}
+ /**
+ * Get Restriction List HTML
+ *
+ * @param Restriction[] $restrictions
+ * @param stdClass $row
+ *
+ * @return string
+ */
+ private static function getRestrictionListHTML(
+ array $restrictions,
+ stdClass $row
+ ) {
+ $items = [];
+
+ foreach ( $restrictions as $restriction ) {
+ if ( $restriction->getBlockId() !== (int)$row->ipb_id ) {
+ continue;
+ }
+
+ if ( $restriction->getType() !== 'page' ) {
+ continue;
+ }
+
+ $items[] = HTML::rawElement(
+ 'li',
+ [],
+ Linker::link( $restriction->getTitle() )
+ );
+ }
+
+ if ( empty( $items ) ) {
+ return '';
+ }
+
+ return Html::rawElement(
+ 'ul',
+ [],
+ implode( '', $items )
+ );
+ }
+
function getQueryInfo() {
$commentQuery = CommentStore::getStore()->getJoin( 'ipb_reason' );
$actorQuery = ActorMigration::newMigration()->getJoin( 'ipb_by' );
'ipb_deleted',
'ipb_block_email',
'ipb_allow_usertalk',
+ 'ipb_sitewide',
] + $commentQuery['fields'] + $actorQuery['fields'],
'conds' => $this->conds,
'join_conds' => [
$lb = new LinkBatch;
$lb->setCaller( __METHOD__ );
+ $partialBlocks = [];
foreach ( $result as $row ) {
$lb->add( NS_USER, $row->ipb_address );
$lb->add( NS_USER_TALK, $row->ipb_address );
$lb->add( NS_USER, $row->by_user_name );
$lb->add( NS_USER_TALK, $row->by_user_name );
}
+
+ if ( !$row->ipb_sitewide ) {
+ $partialBlocks[] = $row->ipb_id;
+ }
+ }
+
+ if ( $partialBlocks ) {
+ // Mutations to the $row object are not persisted. The restrictions will
+ // need be stored in a separate store.
+ $this->restrictions = BlockRestriction::loadByBlockId( $partialBlocks );
}
$lb->execute();
self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
self::FILENAME_TOO_LONG => 'filename-toolong',
];
- if ( isset( $code_to_status[$error] ) ) {
- return $code_to_status[$error];
- }
-
- return 'unknown-error';
+ return $code_to_status[$error] ?? 'unknown-error';
}
/**
*/
public static function getDefaultOption( $opt ) {
$defOpts = self::getDefaultOptions();
- if ( isset( $defOpts[$opt] ) ) {
- return $defOpts[$opt];
- } else {
- return null;
- }
+ return $defOpts[$opt] ?? null;
}
/**
* Check if user is blocked from editing a particular article
*
* @param Title $title Title to check
- * @param bool $bFromSlave Whether to check the replica DB instead of the master
+ * @param bool $fromSlave Whether to check the replica DB instead of the master
* @return bool
*/
- public function isBlockedFrom( $title, $bFromSlave = false ) {
- global $wgBlockAllowsUTEdit;
+ public function isBlockedFrom( $title, $fromSlave = false ) {
+ $blocked = $this->isHidden();
- $blocked = $this->isBlocked( $bFromSlave );
- $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
- // If a user's name is suppressed, they cannot make edits anywhere
- if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName()
- && $title->getNamespace() == NS_USER_TALK ) {
- $blocked = false;
- wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
+ if ( !$blocked ) {
+ $block = $this->getBlock( $fromSlave );
+ if ( $block ) {
+ $blocked = $block->preventsEdit( $title );
+ }
}
+ // only for the purpose of the hook. We really don't need this here.
+ $allowUsertalk = $this->mAllowUsertalk;
+
Hooks::run( 'UserIsBlockedFrom', [ $this, $title, &$blocked, &$allowUsertalk ] );
return $blocked;
*/
public function isHidden() {
if ( $this->mHideName !== null ) {
- return $this->mHideName;
+ return (bool)$this->mHideName;
}
$this->getBlockedStatus();
if ( !$this->mHideName ) {
$this->mHideName = $authUser && $authUser->isHidden();
Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
}
- return $this->mHideName;
+ return (bool)$this->mHideName;
}
/**
return $this->mBlock && $this->mBlock->prevents( 'sendemail' );
}
+ /**
+ * Get whether the user is blocked from using Special:Upload
+ *
+ * @return bool
+ */
+ public function isBlockedFromUpload() {
+ $this->getBlockedStatus();
+ return $this->mBlock && $this->mBlock->prevents( 'upload' );
+ }
+
/**
* Get whether the user is allowed to create an account.
* @return bool
* @return CryptRand
*/
protected static function singleton() {
+ wfDeprecated( __METHOD__, '1.32' );
return MediaWikiServices::getInstance()->getCryptRand();
}
* @return bool Always true
*/
public static function wasStrong() {
+ wfDeprecated( __METHOD__, '1.32' );
return true;
}
* @return string Raw binary random data
*/
public static function generate( $bytes ) {
+ wfDeprecated( __METHOD__, '1.32' );
return random_bytes( floor( $bytes ) );
}
# NOTE: $gis[2] contains a code for the image type. This is no longer used.
$info['width'] = $gis[0];
$info['height'] = $gis[1];
- if ( isset( $gis['bits'] ) ) {
- $info['bits'] = $gis['bits'];
- } else {
- $info['bits'] = 0;
- }
+ $info['bits'] = $gis['bits'] ?? 0;
return $info;
}
--- /dev/null
+<?php
+
+namespace MediaWiki\Widget;
+
+use OOUI\MultilineTextInputWidget;
+
+/**
+ * Widget to select multiple titles.
+ *
+ * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license MIT
+ */
+class TitlesMultiselectWidget extends \OOUI\Widget {
+
+ protected $titlesArray = [];
+ protected $inputName = null;
+ protected $inputPlaceholder = null;
+
+ /**
+ * @param array $config Configuration options
+ * - array $config['titles'] Array of titles to use as preset data
+ * - array $config['placeholder'] Placeholder message for input
+ * - array $config['name'] Name attribute (used in forms)
+ */
+ public function __construct( array $config = [] ) {
+ parent::__construct( $config );
+
+ // Properties
+ if ( isset( $config['default'] ) ) {
+ $this->titlesArray = $config['default'];
+ }
+ if ( isset( $config['name'] ) ) {
+ $this->inputName = $config['name'];
+ }
+ if ( isset( $config['placeholder'] ) ) {
+ $this->inputPlaceholder = $config['placeholder'];
+ }
+
+ $textarea = new MultilineTextInputWidget( [
+ 'name' => $this->inputName,
+ 'value' => implode( "\n", $this->titlesArray ),
+ 'rows' => 10,
+ ] );
+ $this->appendContent( $textarea );
+ $this->addClasses( [ 'mw-widgets-titlesMultiselectWidget' ] );
+ }
+
+ protected function getJavaScriptClassName() {
+ return 'mw.widgets.TitlesMultiselectWidget';
+ }
+
+ public function getConfig( &$config ) {
+ if ( $this->titlesArray !== null ) {
+ $config['selected'] = $this->titlesArray;
+ }
+ if ( $this->inputName !== null ) {
+ $config['name'] = $this->inputName;
+ }
+ if ( $this->inputPlaceholder !== null ) {
+ $config['placeholder'] = $this->inputPlaceholder;
+ }
+
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
+
+}
$interwiki = $this->iwLookup->fetch( $iwPrefix );
$parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) );
- if ( isset( $this->customCaptions[$iwPrefix] ) ) {
- $caption = $this->customCaptions[$iwPrefix];
- } else {
- $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped();
- }
+ $caption = $this->customCaptions[$iwPrefix] ??
+ $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped();
$searchLink = Html::rawElement( 'em', null,
Html::rawElement( 'a', [ 'href' => $href, 'target' => '_blank' ], $caption )
*/
/**
- * Parser for rules of language conversion , parse rules in -{ }- tag.
+ * Parser for rules of language conversion, parse rules in -{ }- tag.
* @ingroup Language
* @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
*/
public $mConverter; // LanguageConverter object
public $mRuleDisplay = '';
public $mRuleTitle = false;
- public $mRules = '';// string : the text of the rules
+ public $mRules = ''; // string : the text of the rules
public $mRulesAction = 'none';
public $mFlags = [];
public $mVariantFlags = [];
public $mConvTable = [];
- public $mBidtable = [];// array of the translation in each variant
- public $mUnidtable = [];// array of the translation in each variant
+ public $mBidtable = []; // array of the translation in each variant
+ public $mUnidtable = []; // array of the translation in each variant
/**
* @param string $text The text between -{ and }-
return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
}
- /**
- * @param string $image
- * @return array|null
- */
- function getImageFile( $image ) {
- return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
- }
-
- /**
- * @return array
- * @since 1.24
- */
- public function getImageFiles() {
- return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
- }
-
/**
* @return array
*/
"exception-nologin-text": "Droëneuh suwah [[Special:Userlogin|neutamöng]] mangat jeuët neupeuhah laman nyoë",
"virus-unknownscanner": "Antivirus hana geuturi:",
"logouttext": "'''Droeneuh ka neutubiet log.'''\n\nBeuneuteupue meunyoe na padum-padum laman nyang deuh lagèe na neutamöng log, sampoe ka lheuh neupeugléh ''cache''.",
- "cannotlogoutnow-title": "H`an jeuet teubiet log jinoe",
+ "cannotlogoutnow-title": "H'an jeuet teubiet jinoe",
"welcomeuser": "Seulamat trôk teuka, $1 !",
- "welcomecreation-msg": "Nan droëneuh ka geupeugöt. \nBèk tuwo neuatô [[Special:Preferences|geunalak {{SITENAME}}]] droëneuh.",
+ "welcomecreation-msg": "Akun-neuh ka geupeugöt. \nDroeneuh jeuet neugantoe {{SITENAME}} [[Special:Preferences|peuatô]] meunyö neumeuh'eut.",
"yourname": "Ureuëng ngui:",
"userlogin-yourname": "Ureuëng ngui",
"userlogin-yourname-ph": "Peutamöng nan ureuëng ngui droëneuh",
"createacct-yourpasswordagain-ph": "Pasoë lom lageuëm rahsia",
"userlogin-remembermypassword": "Pubiyeuë lôn tamöng",
"userlogin-signwithsecure": "Ngui koneksi aman",
- "cannotlogin-title": "H`an jeuet tamong log",
+ "cannotlogin-title": "H'an jeuet tamöng",
+ "cannotloginnow-title": "H'an jeuet tamöng jinoe",
+ "cannotcreateaccount-title": "H'an jeuet peugöt akun",
"yourdomainname": "Domain droeneuh:",
"password-change-forbidden": "Droëneuh h‘an jeuët neuubah lageuëm rahsia bak wiki nyoë.",
- "externaldberror": "Na seunalah bak peusahèh basis data luwa atawa droëneuh hana geubri idin keu neupeubarô akun luwa droëneuh",
+ "externaldberror": "Na seunalah bak peusahèh basis data luwa atawa droëneuh hana geubri idin keu neupubarô akun luwa droëneuh",
"login": "Tamöng",
- "nav-login-createaccount": "Tamöng / dapeuta",
+ "nav-login-createaccount": "Tamöng / peugöt akun",
"logout": "Teubiët",
"userlogout": "Teubiët",
- "notloggedin": "Hana tamöng lom",
- "userlogin-noaccount": "Goh lom neudapeuta?",
+ "notloggedin": "Goh lom neutamöng",
+ "userlogin-noaccount": "Goh lom na akun?",
"userlogin-joinproject": "Neugabông ngön {{SITENAME}}",
"createaccount": "Peudapeuta nan barô",
"userlogin-resetpassword-link": "Tuwö lageuëm rahsia?",
"userlogin-helplink2": "Beunantu tamöng log",
"userlogin-loggedin": "Droëneuh ka neutamöng seubagoë $1. Neungui blangko di yup keu neutamöng seubagoë ureuëng ngui la’én",
- "userlogin-createanother": "Peudapeuta nan barô",
+ "userlogin-createanother": "Peugöt akun laén",
"createacct-emailrequired": "Alamat surat-e",
"createacct-emailoptional": "Alamat surat-e (hana wajéb)",
"createacct-email-ph": "Neupasoë alamat surat-e droëneuh",
"createacct-realname": "Nan aseuli (hana wajéb)",
"createacct-reason": "Alasan:",
"createacct-reason-ph": "Pakön droëneuh neupeugöt nan ureuëng ngui la’én",
- "createacct-submit": "Peudapeuta nan barô",
+ "createacct-submit": "Peugöt akun Droeneuh",
"createacct-another-submit": "Peugöt nan ureuëng ngui la’én",
+ "createacct-continue-submit": "Lanjut pumeugöt akun",
"createacct-benefit-heading": "{{SITENAME}} geupeugöt lé ureuëng lagèë droëneuh.",
"createacct-benefit-body1": "{{PLURAL:$1|peusaneut}}",
"createacct-benefit-body2": "{{PLURAL:$1|$1 halaman}}",
"badretype": "Lageuëm rahsia nyang neupasoë salah.",
"userexists": "Nan ureuëng ngui nyang neupasoë ka na soë ngui.\nNeupiléh nan nyang la'én.",
"loginerror": "Salah bak tamöng",
- "createacct-error": "Peudapeuta nan barô hana meuhasé",
- "createaccounterror": "H‘an jeuët peudapeuta nan: $1",
+ "createacct-error": "Pumeugöt akun hana meuhasé",
+ "createaccounterror": "H'an jeuet peugöt akun: $1",
"nocookiesnew": "Nan ureueng ngui nyoe ka meupeugöt, tapi goh meutamöng.\n{{SITENAME}} jingui ''cookies'' keu peutamöng ureueng ngui.\n''Cookies'' droeneuh hana meupeuudép.\nNeupeuudép ''cookies'' dilèe, lheuh nyan neutamöng ngön nan ureueng ngui ngön lageuem rahsia droeneuh.",
"noname": "Nan ureuëng ngui nyang Droënueh peutamöng hana sah.",
"loginsuccesstitle": "Meuhasé tamöng log",
"loginsuccess": "'''Droëneuh jinoë ka neutamöng di {{SITENAME}} sibagoë \"$1\".'''",
- "nosuchuser": "Hana ureuëng ngui ngön nan \"$1\".\nHaraih rayek ngön haraih ubeut na peungarôh.\nTulông neuparéksa keulayi ijaan-neuh, atawa [[Special:CreateAccount|neudapeuta barô]].",
+ "nosuchuser": "Hana ureuëng ngui ngön nan \"$1\".\nHaraih rayek ngön haraih ubeut na peungarôh.\nNeuparéksa ijaan-neuh, atawa [[Special:CreateAccount|neupeugöt akun]].",
"nosuchusershort": "Hana ureuëng ngui ngön nan \"$1\".\nPréksa keulayi neu’ija Droëneuh.",
"nouserspecified": "Neupasoë nan Droëneuh.",
"login-userblocked": "Ureuëng ngui nyoë ka teublokir, hana idin/hanjeut tamöng.",
"noemail": "Hana alamat surat-e nyang teucatat keu ureuëng ngui \"$1\".",
"noemailcreate": "Droeneuh suwah neuseudia alamt surat-e nyang jeut ngui.",
"passwordsent": "Lageuëm barô ka geupeu'et u surat-e nyang geupeudapeuta keu \"$1\". Neutamöng teuma lheuëh neuteurimöng surat-e nyan.",
- "eauthentsent": "Saboh surat-e keu peunyö ka geukirém u alamat surat-e Droëneuh. Droëneuh beuneuseutöt préntah lam surat nyan keu neupeunyö meunyö alamat nyan nakeuh beutôi atra Droëneuh. {{SITENAME}} h‘an geupeuudép surat Droëneuh meunyö langkah nyoë hana neupeubuet lom.",
+ "eauthentsent": "Saboh surat-e keu peusahèh ka geupeuét u alamat surat-e neuh. Sigohlom surat-e laén geupeuét u akun, Droëneuh beu neuseutöt préntah lam surat nyan, keu neupeusahèh meunyö akun nyan keubit atra Droeneuh.",
"cannotchangeemail": "Alamat surat-e han jeut geugantoe bak wiki nyoe.",
"emaildisabled": "Situs nyoe han jeut geukirém surat-e.",
- "accountcreated": "Ureuëng ngui ka teupeugöt",
- "accountcreatedtext": "Ureuëng ngui keu [[{{ns:User}}:$1|$1]]([[{{ns:User talk}}:$1|talk]]) ka teupeugöt.",
+ "accountcreated": "Akun ka geupeugöt",
+ "accountcreatedtext": "Akun ureuëng ngui keu [[{{ns:User}}:$1|$1]] ([[{{ns:User talk}}:$1|marit]]) ka geupeugöt.",
"createaccount-title": "Peugöt ureuëng ngui keu {{SITENAME}}",
"login-throttled": "Droeneuh ka lé that neuujoe tamöng.\nNeuprèh $1 sigohlom neuujoe lom.",
"login-abort-generic": "Log tamöng droëneuh han meuhasé- Ngon ka geupeubateuë.",
"user-mail-no-body": "Droëneuh ka neucuba kirém e-surat soh ngon that paneuk",
"changepassword": "Gantoe lageuem rahsia",
"resetpass_announce": "Keu neutamöng log, droëneuh suwah neupeugöt lageuëm rahsia barô",
- "resetpass_header": "Gantoë lageuëm rahsia nan ureuëng ngui",
+ "resetpass_header": "Gantoë lageuëm rahsia akun",
"oldpassword": "Lageuëm rahsia awai:",
"newpassword": "Lageuëm rahsia barô:",
"retypenew": "Pasoë lom lageuëm barô:",
"template-protected": "(geulindông)",
"template-semiprotected": "(siteungoh-lindông)",
"hiddencategories": "Laman nyoë nakeuh anggèëta nibak {{PLURAL:$1|1 kawan teusom |$1 kawan teusom}}:",
- "nocreatetext": "{{SITENAME}} ka jitham bak pumeugöt laman barô. \nDroëneuh jeuët neuriwang ngön neupeusaneut laman nyang ka na, atawa [[Special:UserLogin|neutamong log atawa neupeugöt akun]].",
+ "nocreatetext": "{{SITENAME}} ka jitham bak pumeugöt laman barô. \nDroëneuh jeuët neuriwang ngön neupeusaneut laman nyang ka na, atawa [[Special:UserLogin|neutamöng atawa neudapeuta]].",
"nocreate-loggedin": "Droeneuh hana khut keu neupeugöt laman-laman barô.",
"sectioneditnotsupported-title": "Peusaneut bideueng hana geudukông",
"sectioneditnotsupported-text": "Peusaneut bideueng hana geudukông bak laman nyoe.",
"preferences": "Galak",
"mypreferences": "Atô",
"prefs-edits": "Jumeulah neuandam:",
+ "prefsnologintext2": "Neutamöng mangat jeuet neugantoe peuatô",
"prefs-skin": "Kulét",
"skin-preview": "Eu dilèe",
"datedefault": "Hana geunalak",
"duration-seconds": "$1 {{PLURAL:$1|Sekunde|Sekunden}}",
"duration-minutes": "$1 {{PLURAL:$1|Minute|Minuten}}",
"duration-hours": "$1 {{PLURAL:$1|Stunde|Stunden}}",
- "duration-days": "$1 {{PLURAL:$1|Tag|Tage}}",
+ "duration-days": "$1 {{PLURAL:$1|Tag|Tagen}}",
"duration-weeks": "$1 {{PLURAL:$1|Woche|Wochen}}",
"duration-years": "$1 {{PLURAL:$1|Jahr|Jahre}}",
"duration-decades": "$1 {{PLURAL:$1|Jahrzehnt|Jahrzehnte}}",
"tog-extendwatchlist": "Expand watchlist to show all changes, not just the most recent",
"tog-usenewrc": "Group changes by page in recent changes and watchlist",
"tog-numberheadings": "Auto-number headings",
- "tog-showtoolbar": "Show edit toolbar",
"tog-editondblclick": "Edit pages on double click",
"tog-editsectiononrightclick": "Enable section editing by right clicking on section titles",
"tog-watchcreations": "Add pages I create and files I upload to my watchlist",
"subject-preview": "Preview of subject:",
"previewerrortext": "An error occurred while attempting to preview your changes.",
"blockedtitle": "User is blocked",
+ "blocked-email-user": "<strong>Your username has been blocked from sending email. You can still edit other pages on this wiki.</strong> You can view the full block details at [[Special:MyContributions|account contributions]].\n\nThe block was made by $1.\n\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n* Block ID #$5",
+ "blockedtext-partial": "<strong>Your username or IP address has been blocked from making changes to this page. You can still edit other pages on this wiki.</strong> You can view the full block details at [[Special:MyContributions|account contributions]].\n\nThe block was made by $1.\n\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n* Block ID #$5",
"blockedtext": "<strong>Your username or IP address has been blocked.</strong>\n\nThe block was made by $1.\nThe reason given is <em>$2</em>.\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou can contact $1 or another [[{{MediaWiki:Grouppage-sysop}}|administrator]] to discuss the block.\nYou cannot use the \"{{int:emailuser}}\" feature unless a valid email address is specified in your [[Special:Preferences|account preferences]] and you have not been blocked from using it.\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
"autoblockedtext": "Your IP address has been automatically blocked because it was used by another user, who was blocked by $1.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYou may contact $1 or one of the other [[{{MediaWiki:Grouppage-sysop}}|administrators]] to discuss the block.\n\nNote that you may not use the \"{{int:emailuser}}\" feature unless you have a valid email address registered in your [[Special:Preferences|user preferences]] and you have not been blocked from using it.\n\nYour current IP address is $3, and the block ID is #$5.\nPlease include all above details in any queries you make.",
"systemblockedtext": "Your username or IP address has been automatically blocked by MediaWiki.\nThe reason given is:\n\n:<em>$2</em>\n\n* Start of block: $8\n* Expiration of block: $6\n* Intended blockee: $7\n\nYour current IP address is $3.\nPlease include all above details in any queries you make.",
"ipb-disableusertalk": "Prevent this user from editing their own talk page while blocked",
"ipb-change-block": "Re-block the user with these settings",
"ipb-confirm": "Confirm block",
+ "ipb-sitewide": "Sitewide",
+ "ipb-partial": "Partial",
+ "ipb-type-label": "Type",
+ "ipb-pages-label": "Pages",
"badipaddress": "Invalid IP address",
"blockipsuccesssub": "Block succeeded",
"blockipsuccesstext": "[[Special:Contributions/$1|$1]] has been blocked.<br />\nSee the [[Special:BlockList|block list]] to review blocks.",
"createaccountblock": "account creation disabled",
"emailblock": "email disabled",
"blocklist-nousertalk": "cannot edit own talk page",
+ "blocklist-editing": "editing",
+ "blocklist-editing-sitewide": "editing (sitewide)",
"ipblocklist-empty": "The block list is empty.",
"ipblocklist-no-results": "The requested IP address or username is not blocked.",
"blocklink": "block",
"logentry-block-block": "$1 {{GENDER:$2|blocked}} {{GENDER:$4|$3}} with an expiration time of $5 $6",
"logentry-block-unblock": "$1 {{GENDER:$2|unblocked}} {{GENDER:$4|$3}}",
"logentry-block-reblock": "$1 {{GENDER:$2|changed}} block settings for {{GENDER:$4|$3}} with an expiration time of $5 $6",
+ "logentry-partialblock-block": "$1 {{GENDER:$2|blocked}} {{GENDER:$4|$3}} from editing {{PLURAL:$8||the pages}} $7 with an expiration time of $5 $6",
+ "logentry-partialblock-reblock": "$1 {{GENDER:$2|changed}} block settings for {{GENDER:$4|$3}} preventing edits on {{PLURAL:$8||the pages}} $7 with an expiration time of $5 $6",
+ "logentry-non-editing-block-block": "$1 {{GENDER:$2|blocked}} {{GENDER:$4|$3}} from non-editing actions with an expiration time of $5 $6",
+ "logentry-non-editing-block-reblock": "$1 {{GENDER:$2|changed}} block settings for {{GENDER:$4|$3}} for non-editing actions with an expiration time of $5 $6",
"logentry-suppress-block": "$1 {{GENDER:$2|blocked}} {{GENDER:$4|$3}} with an expiration time of $5 $6",
"logentry-suppress-reblock": "$1 {{GENDER:$2|changed}} block settings for {{GENDER:$4|$3}} with an expiration time of $5 $6",
"logentry-import-upload": "$1 {{GENDER:$2|imported}} $3 by file upload",
"mw-widgets-titleinput-description-redirect": "redirect to $1",
"mw-widgets-categoryselector-add-category-placeholder": "Add a category...",
"mw-widgets-usersmultiselect-placeholder": "Add more...",
+ "mw-widgets-titlesmultiselect-placeholder": "Add more...",
"date-range-from": "From date:",
"date-range-to": "To date:",
"sessionmanager-tie": "Cannot combine multiple request authentication types: $1.",
"collapsible-collapse": "Roupliyé",
"collapsible-expand": "Dévlopé",
"confirmable-confirm": "Ès zòt sir{{GENDER:$1||}} ?",
- "confirmable-yes": "Wi",
+ "confirmable-yes": "Enren",
"confirmable-no": "Awa",
"thisisdeleted": "Ès zòt ka déziré afiché oben rèstoré $1 ?",
"viewdeleted": "Wè $1 ?",
"badarticleerror": "Sa agsyon pa pouvé fika éfègtchwé asou sa paj.",
"cannotdelete": "Enposib di siprimen paj-a oben fiché-a « $1 ».\nSiprésyon-an pitèt ja té éfègtchwé pa rounòt moun.",
"cannotdelete-title": "Enposib di siprimen paj-a « $1 »",
+ "delete-scheduled": "Paj-a « $1 » sa progranmen pou fika siprimen.\nSouplé, pasyanté.",
"delete-hook-aborted": "Siprésyon annilé pa roun ègstansyon.\nPyès lèsplikasyon té fourni.",
"no-null-revision": "Enposib di kréyé roun nouvèl révizyon vid pou paj-a « $1 »",
"badtitle": "Movè tit",
"customcssprotected": "Zòt pa gen pèrmisyon-an di modifyé sa féy di èstil CSS, pas li ka kontni paranmèt pésonnèl-ya di rounòt itilizatò.",
"customjsonprotected": "Zòt pa gen drwè di modifyé sa paj JSON pas li ka kontni paranmèt pésonnèl-ya di rounòt itilizatò.",
"customjsprotected": "Zòt pa gen pèrmisyon-an di modifyé sa paj di JavaScript, pas li ka kontni paranmèt pésonnèl-ya di rounòt itilizatò.",
+ "sitecssprotected": "Zòt pa gen drwè di modifyé sa paj CSS pas sa pouvé afègté tout vizitò-ya.",
+ "sitejsonprotected": "Zòt pa gen drwé di modifyé sa paj JSON pas sa pouvé afègté tout vizitò-ya.",
+ "sitejsprotected": "Zòt pa gen drwè di modifyé sa paj JavaScript pas sa pouvé afègté tout vizitò-ya.",
"mycustomcssprotected": "Zòt pa gen drwè di modifyé sa paj CSS.",
"mycustomjsonprotected": "Zòt pa gen drwè di modifyé sa paj JSON.",
"mycustomjsprotected": "Zòt pa gen drwè di modifyé sa paj JavaScript.",
"login-migrated-generic": "Zòt kont té migré, é zòt non d'itilizatò pa ka ègzisté òkò asou sa wiki.",
"loginlanguagelabel": "Lanng : $1",
"suspicious-userlogout": "Zòt doumann di konnègsyon té roufizé pas i sanblé ki li té voyé pa roun navigatò défègtché oben dipi kach-a di roun sèrvis mandatèr.",
- "createacct-another-realname-tip": "Véritab non sa òpsyonèl.\nSi zòt désidé di fourni li, i ké fika itilizé pou krédité lotò di so travay.",
+ "createacct-another-realname-tip": "Véritab non-an sa òpsyonnèl.\nSi zòt désidé di fourni li, i ké fika itilizé pou krédité lotò-a di so travay-ya.",
"pt-login": "Konnègté so kò",
"pt-login-button": "Konnègté so kò",
"pt-login-continue-button": "Kontinwé konnègsyon-an",
"botpasswords-restriction-failed": "Rèstrigsyon-yan di modipas di robo ka anpéché sa konnègsyon.",
"botpasswords-invalid-name": "Non-an d'itilizatò spésifyé pa ka kontni di séparatò di mo di pas di robo (« $1 »).",
"botpasswords-not-exist": "{{GENDER:$1|Itilizatò|Itilizatris}}-a « $1 » pa gen di mo di pas di robo nonmen « $2 ».",
+ "botpasswords-needs-reset": "Modipas-a di robo di non « $2 » di itilizatò-a « $1 » divèt fika réynisyalizé.",
"resetpass_forbidden": "Mo di pas pa pouvé fika chanjé.",
"resetpass_forbidden-reason": "Mo di pas pa pouvé fika modifyé : $1",
"resetpass-no-info": "Zòt divèt fika konnègté pou agsédé dirèkman à sa paj.",
"editing": "Modifikasyon di $1",
"creating": "Kréyasyon di $1",
"editingsection": "Modifikasyon di $1 (sèksyon)",
+ "yourtext": "Zòt tègs",
+ "yourdiff": "Diférans",
"templatesused": "{{PLURAL:$1|Modèl itilizé}} pa sa paj :",
"templatesusedpreview": "{{PLURAL:$1|Modèl itilizé}} annan sa prévizwalizasyon :",
"template-protected": "(protéjé)",
"permissionserrorstext-withaction": "Zòt pa pouvé $2, pou {{PLURAL:$1|rézon swivant}} :",
"recreate-moveddeleted-warn": "<strong>Panga : zòt ka roukréyé roun paj ki té présédanman siprimen.</strong>\n\nAsouré-zòt ki i sa pèrtinan di pourswiv modifikasyon-yan asou sa paj.\nJournal-ya dé siprésyon é dé déplasman pou sa paj sa fourni isi pou lenfòrmasyon :",
"moveddeleted-notice": "Sa paj té siprimen. \nJournal-ya dé siprésyon, dé protègsyon é dé déplasman pou paj-a sa afiché anba pou référans.",
+ "edit-conflict": "Trafalga di modifikasyon.",
+ "postedit-confirmation-created": "Paj-a té fika kréyé.",
+ "invalid-content-data": "Data di kontni pa valid",
"content-model-wikitext": "wikitèks",
+ "content-model-text": "tègs groso",
+ "content-model-javascript": "JavaScript",
+ "content-json-empty-object": "Lòbjè vid",
+ "content-json-empty-array": "Tablo vid",
"undo-failure": "Sa modifikasyon pa pouvé fika défè : sa-a té ké rantré an konfli ké modifikasyon entèrmédjèr-ya.",
"viewpagelogs": "Wè opérasyon-yan asou sa paj",
+ "nohistory": "I pa ka ègzisté di listorik dé modifikasyon pou sa paj.",
+ "currentrev": "Vèrsyon atchwèl",
"currentrev-asof": "Vèrsyon atchwèl daté di $1",
"revisionasof": "Vèrsyon di $1",
"revision-info": "Révizyon daté di $1 pa {{GENDER:$6|$2}}$7",
"nextrevision": "Vèrsyon swivant →",
"currentrevisionlink": "Wè vèrsyon atchwèl-a",
"cur": "atch",
+ "next": "swivan",
"last": "dif",
+ "page_first": "pronmyé",
+ "page_last": "dannyé",
"histlegend": "Sélègsyon di diff : koché bouton radjo-ya dé vèrsyon ki à konparé é apiyé asou rantré oben asou bouton-an ki anba.<br />\nLéjann : <strong>({{int:cur}})</strong> = diférans ké dannyé vèrsyon-an, <strong>({{int:last}})</strong> = diférans ké vèrsyon présédan-an, <strong>{{int:minoreditletter}}</strong> = modifikasyon minò.",
"history-fieldset-title": "Sasé dé révizyon",
"histfirst": "Pli ansyenn",
"histlast": "Pli résan-yan",
+ "historyempty": "(vid)",
"history-feed-title": "Listorik dé vèrsyon",
"history-feed-description": "Listorik dé vèrsyon pou sa paj asou wiki-a",
"history-feed-item-nocomment": "$1 à $2",
"rev-delundel": "afiché/maské",
+ "rev-showdeleted": "afiché",
+ "revdelete-show-file-submit": "Enren",
+ "revdelete-hide-comment": "Rézimen di modifikasyon",
+ "revdelete-log": "Motif",
+ "pagehist": "Listorik di paj-a",
+ "revdelete-reasonotherlist": "Ròt rézon",
"mergelog": "Journal dé fizyon",
"history-title": "$1 : Listorik dé vèrsyon",
"difference-title": "$1 : Diférans ant vèrsyon",
"searchprofile-articles-tooltip": "Sasé annan $1",
"searchprofile-images-tooltip": "Sasé dé fiché miltimédja",
"searchprofile-everything-tooltip": "Sasé annan tout sit-a (osi annan paj di diskisyon-yan)",
- "searchprofile-advanced-tooltip": "Sasé annan lèspas di non pèrsonalizé",
+ "searchprofile-advanced-tooltip": "Sasé annan lèspas di non-yan ki pésonnalizé",
"search-result-size": "$1 ({{PLURAL:$2|1 mo|$2}})",
"search-result-category-size": "$1 manm{{PLURAL:$1|}} ($2 soukatégori{{PLURAL:$2|}}, $3 fiché{{PLURAL:$3|}})",
"search-redirect": "(Roudirègsyon dipi $1)",
"speciallogtitlelabel": "Sib (tit oben {{ns:user}}:non di itilizatò) :",
"log": "Journal d’opérasyon",
"all-logs-page": "Tout journal piblik",
- "alllogstext": "Lafichaj konbinen di tout journal-ya ki disponnib asou {{SITENAME}}.\nZòt pouvé pèrsonalizé lafichaj an sélègsyonnan tip di journal-a, non di itilizatò-a oben paj-a ki konsèrnen (sa Dé dannyé sa sansib à lakas).",
+ "alllogstext": "Lafichaj konbinen di tout journal-ya ki disponnib asou {{SITENAME}}.\nZòt pouvé pésonnalizé lafichaj-a an sélègsyonnan tip di journal-a, non di itilizatò-a oben paj-a ki konsèrnen (sa Dé dannyé sa sansib à lakas-a).",
"logempty": "Pyès lopérasyon ki ka korèsponn annan journal-ya.",
"allpages": "Tout paj-ya",
"allarticles": "Tout paj-ya",
"pageinfo-templates": "{{PLURAL:$1|Modèl enkli}} ($1)",
"pageinfo-toolboxlink": "Lenfòrmasyon asou paj-a",
"pageinfo-contentpage": "Konté kou paj di kontni",
- "pageinfo-contentpage-yes": "Wi",
+ "pageinfo-contentpage-yes": "Enren",
"patrol-log-page": "Journal dé roulèktir",
"previousdiff": "← Modifikasyon présédant",
"nextdiff": "Modifikasyon swivant →",
"specialpages": "Paj èspésyal",
"tag-filter": "Filtré [[Special:Tags|baliz]] :",
"tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Baliz}}]] : $2)",
- "tags-active-yes": "Wi",
+ "tags-active-yes": "Enren",
"tags-active-no": "Awa",
"tags-hitcount": "$1 modifikasyon{{PLURAL:$1|}}",
"logentry-delete-delete": "$1 siprimen paj-a $3",
"previewnote": "<strong>Atencez ke ico esas nur prevido.</strong> Ol ne registragesis ankore!",
"continue-editing": "Irez a la redakto-areo",
"session_fail_preview": "'''Pardonez! Ni ne povis traktar vua redakto pro perdo di sesiono donaji.'''\nVoluntez probar itere.\nSe ol ankore nefuncionas, probez [[Special:UserLogout|ekirar]] e pose enirar.",
+ "session_fail_preview_html": "Pardonez! Ni ne povis recevar vua redakto pro perdajo di dati.\n\n<em>Pro ke la wiki {{SITENAME}} permisas uzar bruta HTML, la previdado celesas por preventar ataki uzante JavaScript.</em>\n\n<strong>Se la probo di redakto esas legitima, voluntez itere sendar ol.</strong>\nSe duros ne funcionar, facez [[Special:UserLogout|logout]] ed itere facez login. Videz se vua retonavigilo (browser) permisas uzar 'cookies' de ica retosituo.",
"editing": "Vu redaktas $1",
"creating": "Vu kreas $1",
"editingsection": "Vu redaktas $1 (seciono)",
"Sawmw"
]
},
+ "tog-showtoolbar": "ၮဲဖှ်ေ ဆ်ုအင်းတါင်ခြီခြာ့တိုင်",
"underline-always": "ကိုဲၜၠင်",
"underline-never": "ၮင်းဖိုင့်အေႋ",
"editfont-serif": "ခေါဟ်ထိင်ႋပါ့ဖောင့်",
"noindex-category": "ဝီႋဖၠုံးသံင့်လေဝ်လိက်ဖၠုံးခၞါလ်ုအ်ှ လိက်မေံၜၠါ်လ်ုဖး",
"broken-file-category": "ခါၯာၯံင် ဖိုင်ႋလင့်အှ်သယ်လ်ုဖး လိက်မေံၜၠါ်",
"about": "အ်ုကျံင်",
+ "article": "ပ်ုယုံ့ခေါဟ်တင်လိက်မေံၜၠာ်",
"newwindow": "(ဝင်းဒိုးသင့်လ်ုၮါင်းဝယ် မ်ုပုဂ်ထုင်း)",
"cancel": "မာလှ်ေအေး",
"moredotdotdot": "ၰိုဲမေံၜၠာ်...",
"jumpto": "မ်ုၯယ့်ထါင်ယိုဝ်",
"jumptonavigation": "ပ်ုယုံ့",
"jumptosearch": "အင်းၯူ့",
+ "pool-errorunknown": "လ်ုသီးယာ့ ဆ်ုမးၜး",
+ "poolcounter-usage-error": "ဆ်ုသုံႋဆာႋအ်ုမး: $1",
"aboutsite": "အ်ုကျံင် {{SITENAME}}",
"aboutpage": "Project:အ်ုၯံင်အ်ုကျံင်",
"copyrightpage": "{{ns:project}}: ပ္တုံဆာပၞံင့်",
"showtoc": "ဍုဂ်ၮဲ",
"hidetoc": "အ်ှသူး",
"collapsible-collapse": "မ်ုပေဝ်ႋက္ဍာ",
+ "collapsible-expand": "လဝ်လဲာ",
"confirmable-confirm": "{{GENDER:$1|ၮ်ု}} ထီ့ဆာႋဝး?",
"confirmable-yes": "မွာဲ",
"confirmable-no": "လ်ုမာၜး",
"nosuchspecialpage": "ဗေ့ယိုဝ်သိုဝ် လိက်မေံၜၠါ်ခေါဟ် လ်ုအှ်ၜး",
"nospecialpagetext": "<strong>ၮ်ုယိုဝ် လ်ုထီ့ဆာ့ၜး လိက်မေံခေါဟ်လ်ုၮါင်းအိုဝ် အင်းကိင်ဖှ်ေထဆေဝ်ႋလှ်။</strong>\n\nထီ့ဆာ့ လိက်မေံခေါဟ် စ်ုရင့်သယ် [[Special:SpecialPages|{{int:specialpages}}]] ခဝ့် ၮ်ုဍးၮေဝ်လှ်။",
"error": "ဆ်ုမး",
+ "databaseerror-error": "အ်ုမး: $1",
"badtitle": "လိက်မေံဆ်ုနာႋ",
"badtitletext": "အင်းကိင်ႋလင်ထ လိက်မေံၜၠါ် ခေါဟ်တင်ၮ်ှ လ်ုဖံင်ပၞံင့် (လ်ု) လ်ုအှ်မိင်ၜး (လ်ု) ၰာၰံင်ဘာႋသာ့လ်ုဖး(inter-language or inter-wiki title)အိုဝ် ထိုဝ်ၜုဂ်လင့်မးဝေ့လှ်။",
"viewsource": "မ်ုယောဝ်ႋအ်ုဝီခၞာ",
"imagetypemismatch": "De nuje bestandjsextensie is neet gliek aan 't bestandjstype.",
"imageinvalidfilename": "De nuje bestandsnaam is ongeldig",
"fix-double-redirects": "Alle doorverwiezinge biewerke die verwieze nao de originele paginanaam",
- "move-leave-redirect": "'n Doorverwiezing achterlaote",
+ "move-leave-redirect": "Laot 'ne redirek staon",
"protectedpagemovewarning": "'''Waorsjoewing: Dees pazjena is besjermp zoedat ze allein doer gebroekers mit administratorrechte kint weure verplaats.'''\nDe lèste logbookregel steit hierónger:",
"semiprotectedpagemovewarning": "<strong>Let op:</strong> Dees pazjena is beveilig en kin allein door geregistreerde gebroekers verplaats waere.\nDe lèste logbookregel steit hiejónger:",
"move-over-sharedrepo": "[[:$1]] besteit al in 'ne gedeildje mediadatabank.\nE bestandj hiehaer verplaatse euversjrief 't gedeildj bestandj.",
"badarticleerror": "Deze handeling kan niet op deze pagina worden uitgevoerd.",
"cannotdelete": "De pagina of het bestand \"$1\" kon niet verwijderd worden.\nMogelijk is deze al door iemand anders verwijderd.",
"cannotdelete-title": "Pagina \"$1\" kan niet verwijderd worden",
+ "delete-scheduled": "De pagina \"$1\" staat voor verwijdering ingepland.\nEen ogenblik geduld alstublieft.",
"delete-hook-aborted": "Het verwijderen is afgebroken door een hook.\nEr is geen toelichting beschikbaar.",
"no-null-revision": "Het was niet mogelijk een lege nieuwe versie te maken voor de pagina \"$1\"",
"badtitle": "Ongeldige paginanaam",
"passwordpolicies-policy-passwordcannotmatchusername": "Wachtwoord mag niet hetzelfde zijn als de gebruikersnaam",
"passwordpolicies-policy-passwordcannotmatchblacklist": "Wachtwoord mag niet overeenkomen met wachtwoorden op de zwarte lijst",
"passwordpolicies-policy-maximalpasswordlength": "Wachtwoord moet minder dan $1 {{PLURAL:$1|teken|tekens}} bevatten",
- "passwordpolicies-policy-passwordcannotbepopular": "Watchwoord mag niet {{PLURAL:$1|overeenkomen met het bekende wachtwoord|voorkomen in de lijst met $1 bekende wachtwoorden}}"
+ "passwordpolicies-policy-passwordcannotbepopular": "Watchwoord mag niet {{PLURAL:$1|overeenkomen met het bekende wachtwoord|voorkomen in de lijst met $1 bekende wachtwoorden}}",
+ "unprotected-js": "Vanwege veiligheidsredenen kan er geen JavaScript geladen worden vanaf onbeveiligde pagina's. Gelieve alleen JavaScript pagina's aan te maken in de MediaWiki: naamruimte of als een subpagina van een gebruikerspagina."
}
"and": " e",
"faq": "Perguntas frequentes",
"actions": "Ações",
- "namespaces": "Domínios",
+ "namespaces": "Espaços nominais",
"variants": "Variantes",
"navigation-heading": "Menu de navegação",
"errorpagetitle": "Erro",
"email-allow-new-users-label": "Permitir mensagens de correio de utilizadores novos",
"email-blacklist-label": "Proibir estes utilizadores de me enviarem correio eletrónico:",
"prefs-searchoptions": "Pesquisa",
- "prefs-namespaces": "Domínios",
+ "prefs-namespaces": "Espaços nominais",
"default": "padrão",
"prefs-files": "Ficheiros",
"prefs-custom-css": "CSS personalizado",
"prefixindex": "Todas as páginas iniciadas por",
"prefixindex-namespace": "Todas as páginas com prefixo (espaço nominal $1)",
"prefixindex-submit": "Mostrar",
- "prefixindex-strip": "Remover prefixo",
+ "prefixindex-strip": "Esconder o prefixo nos resultados",
"shortpages": "Páginas curtas",
"longpages": "Páginas longas",
"deadendpages": "Páginas sem saída",
"specialpages-group-pagetools": "Ferramentas de página",
"specialpages-group-wiki": "Dados e ferramentas",
"specialpages-group-redirects": "Páginas especiais de redirecionamento",
- "specialpages-group-spam": "Ferramentas anti-spam",
+ "specialpages-group-spam": "Ferramentas antispam",
"specialpages-group-developer": "Ferramentas de desenvolvimento",
"blankpage": "Página em branco",
"intentionallyblankpage": "Esta página foi intencionalmente deixada em branco",
"passwordpolicies-policy-passwordcannotmatchblacklist": "A palavra-passe não pode corresponder às especificamente bloqueadas pela lista negra",
"passwordpolicies-policy-maximalpasswordlength": "A palavra-passe tem de ter menos de $1 {{PLURAL:$1|carácter|caracteres}}",
"passwordpolicies-policy-passwordcannotbepopular": "A palavra-passe não pode {{PLURAL:$1|ser a mais popular|estar na lista das $1 palavras-passe mais populares}}",
- "easydeflate-invaliddeflate": "O conteúdo fornecido não está devidamente comprimido"
+ "easydeflate-invaliddeflate": "O conteúdo fornecido não está devidamente comprimido",
+ "unprotected-js": "Por motivos de segurança o JavaScript de páginas desprotegidas não pode ser carregado. Crie javascript só no espaço nominal/domínio MediaWiki: ou numa subpágina do utilizador"
}
"tog-extendwatchlist": "[[Special:Preferences]], tab 'Watchlist'. Offers user to show all applicable changes in watchlist (by default only the last change to a page on the watchlist is shown). {{Gender}}",
"tog-usenewrc": "{{Gender}}\nUsed as label for the checkbox in [[Special:Preferences]], tab \"Recent changes\".\n\nOffers user to use alternative representation of [[Special:RecentChanges]] and watchlist.",
"tog-numberheadings": "[[Special:Preferences]], tab 'Misc'. Offers numbered headings on content pages to user. {{Gender}}",
- "tog-showtoolbar": "{{Gender}}\n[[Special:Preferences]], tab 'Edit'. Offers user to show edit toolbar in page edit screen.\n\nThis is the toolbar: [[Image:Toolbar.png]]",
"tog-editondblclick": "{{Gender}}\n[[Special:Preferences]], tab 'Edit'. Offers user to open edit page on double click.",
"tog-editsectiononrightclick": "{{Gender}}\n[[Special:Preferences]], tab 'Edit'. Offers user to edit a section by clicking on a section title.",
"tog-watchcreations": "[[Special:Preferences]], tab 'Watchlist'. Offers user to add created pages to watchlist. {{Gender}}",
"subject-preview": "Used as label for preview of the section title when adding a new section on a talk page.\n\nShould match {{msg-mw|subject}}.\n\nSee also:\n* {{msg-mw|Summary-preview}}\n\n{{Identical|Subject}}",
"previewerrortext": "When a user has the editing preference LivePreview enabled, clicked the Preview or Show Changes button in the edit page and the action did not succeed.",
"blockedtitle": "Used as title displayed for blocked users. The corresponding message body is one of the following messages:\n* {{msg-mw|Blockedtext|notext=1}}\n* {{msg-mw|Autoblockedtext|notext=1}}\n* {{msg-mw|Systemblockedtext}}",
+ "blocked-email-user": "Text displayed to partially blocked users that are blocked from sending email.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link)\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Autoblockedtext}}\n* {{msg-mw|Systemblockedtext}}",
+ "blockedtext-partial": "Text displayed to partially blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link)\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Autoblockedtext}}\n* {{msg-mw|Systemblockedtext}}",
"blockedtext": "Text displayed to blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link)\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Autoblockedtext}}\n* {{msg-mw|Systemblockedtext}}",
"autoblockedtext": "Text displayed to automatically blocked users.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - the blocking sysop (with a link to his/her userpage)\n* $2 - the reason for the block (in case of autoblocks: {{msg-mw|autoblocker}})\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the blocking sysop's username (plain text, without the link). Use it for GENDER.\n* $5 - the unique numeric identifier of the applied autoblock\n* $6 - the expiry of the block\n* $7 - the intended target of the block (what the blocking user specified in the blocking form)\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext}}\n* {{msg-mw|Systemblockedtext}}",
"systemblockedtext": "Text displayed to requests blocked by MediaWiki configuration.\n\n\"email this user\" should be consistent with {{msg-mw|Emailuser}}.\n\nParameters:\n* $1 - (Unused) A dummy user attributed as the blocker, possibly as a link to a user page.\n* $2 - the reason for the block\n* $3 - the current IP address of the blocked user\n* $4 - (Unused) the dummy blocking user's username (plain text, without the link).\n* $5 - A short string indicating the type of system block.\n* $6 - the expiry of the block\n* $7 - the intended target of the block\n* $8 - the timestamp when the block started\nSee also:\n* {{msg-mw|Grouppage-sysop}}\n* {{msg-mw|Blockedtext}}\n* {{msg-mw|Autoblockedtext}}",
"ipb-disableusertalk": "{{doc-singularthey}}\nUsed as label for checkbox in [[Special:Block]].\n\nSee also:\n* {{msg-mw|ipbemailban}}\n* {{msg-mw|ipbenableautoblock}}\n* {{msg-mw|ipbhidename}}\n* {{msg-mw|ipbwatchuser}}\n* {{msg-mw|ipb-hardblock}}",
"ipb-change-block": "Confirmation checkbox required for blocks that would override an earlier block. Appears together with {{msg-mw|ipb-needreblock}}.",
"ipb-confirm": "Used as hidden field in the form on [[Special:Block]].",
+ "ipb-sitewide": "A type of block the user can select from on [[Special:Block]].",
+ "ipb-partial": "A type of block the user can select from on [[Special:Block]].",
+ "ipb-type-label": "The label of the type of editing restriction the admin would like to impose on [[Special:Block]].",
+ "ipb-pages-label": "The label for a autocomplete text field to specify pages to block a user from editing on [[Special:Block]].",
"badipaddress": "An error message shown when one entered an invalid IP address in blocking page.",
"blockipsuccesssub": "Used as page title in [[Special:Block]].\n\nThis message is the subject for the following message:\n* {{msg-mw|Blockipsuccesstext}}",
"blockipsuccesstext": "Used in [[Special:Block]].\nThe title (subject) for this message is {{msg-mw|Blockipsuccesssub}}.\n\nParameters:\n* $1 - username, can be used for GENDER",
"createaccountblock": "Part of the log entry of user block in [[Special:BlockList]].\n\nSee also:\n* {{msg-mw|Block-log-flags-nocreate}}\n{{Related|Blocklist}}",
"emailblock": "Part of the log entry of user block in [[Special:BlockList]].\n{{Related|Blocklist}}\n{{Identical|E-mail blocked}}",
"blocklist-nousertalk": "Used in [[Special:IPBlockList]] when \"Allow this user to edit own talk page while blocked\" option hasn't been flagged.\n\nSee also {{msg-mw|Block-log-flags-nousertalk}}.\n\nPart of the log entry of user block in [[Special:BlockList]].\n\n{{Related|Blocklist}}",
+ "blocklist-editing-sitewide": "Used in [[Special:IPBlockList]] when a block is a sitewide block.",
+ "blocklist-editing": "Used in [[Special:IPBlockList]] when a block is not a sitewide block.",
"ipblocklist-empty": "Used in [[Special:BlockList]], if the target is not specified.\n\nSee also:\n* {{msg-mw|Ipblocklist-no-results}}",
"ipblocklist-no-results": "Used in [[Special:BlockList]], if the target is specified.\n\nSee also:\n* {{msg-mw|Ipblocklist-empty}}",
"blocklink": "Display name for a link that, when selected, leads to a form where a user can be blocked. Used in page history and recent changes pages. Example: \"''UserName (Talk | contribs | '''block''')''\".\n\nUsed as link title in [[Special:Contributions]] and in [[Special:DeletedContributions]].\n\nSee also:\n* {{msg-mw|Sp-contributions-talk}}\n* {{msg-mw|Change-blocklink}}\n* {{msg-mw|Unblocklink}}\n* {{msg-mw|Sp-contributions-blocklog}}\n* {{msg-mw|Sp-contributions-uploads}}\n* {{msg-mw|Sp-contributions-logs}}\n* {{msg-mw|Sp-contributions-deleted}}\n* {{msg-mw|Sp-contributions-userrights}}\n{{Identical|Block}}",
"logentry-block-block": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n\nCf. {{msg-mw|Blocklogentry}}",
"logentry-block-unblock": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n\nCf. {{msg-mw|Unblocklogentry}}",
"logentry-block-reblock": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n\nCf. {{msg-mw|Reblock-logentry}}",
+ "logentry-partialblock-block": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n* $7 - list of pages separated by a comma\n* $8 - total number of pages\n\nCf. {{msg-mw|Blocklogentry}}",
+ "logentry-partialblock-reblock": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n* $7 - list of pages separated by a comma\n* $8 - total number of pages\n\nCf. {{msg-mw|Reblock-logentry}}",
+ "logentry-non-editing-block-block": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n\nCf. {{msg-mw|Blocklogentry}}",
+ "logentry-non-editing-block-reblock": "{{Logentry|[[Special:Log/block]]}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string\n\nCf. {{msg-mw|Reblock-logentry}}",
"logentry-suppress-block": "{{Logentry}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string",
"logentry-suppress-reblock": "{{Logentry}}\n* $4 - user name for gender or empty string for autoblocks\n* $5 - the block duration, localized and formatted with the english tooltip\n* $6 - block detail flags or empty string",
"logentry-import-upload": "{{Logentry|[[Special:Log/import]]}}",
"mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.",
"mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.",
"mw-widgets-usersmultiselect-placeholder": "Placeholder displayed in the input field, where new usernames are entered",
+ "mw-widgets-titlesmultiselect-placeholder": "Placeholder displayed in the input field, where new titles are entered",
"date-range-from": "Label for an input field that specifies the start date of a date range filter.",
"date-range-to": "Label for an input field that specifies the end date of a date range filter.",
"sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.",
"tog-watchlisthideminor": "ซ่อนการแก้ไขเล็กน้อยจากรายการเฝ้าดู",
"tog-watchlisthideliu": "ซ่อนการแก้ไขโดยผู้ใช้ล็อกอินจากรายการเฝ้าดู",
"tog-watchlistreloadautomatically": "โหลดรายการเฝ้าดูใหม่อัตโนมัติเมื่อใดที่มีการเปลี่ยนตัวกรอง (ต้องการจาวาสคริปต์)",
- "tog-watchlistunwatchlinks": "à¹\80à¸\9eิà¹\88มลิà¸\87à¸\81à¹\8cà¹\80ลิà¸\81à¹\80à¸\9dà¹\89าà¸\94ู/à¹\80à¸\9dà¹\89าà¸\94ูà¹\82à¸\94ยà¸\95รà¸\87à¹\80à¸\82à¹\89าหà¸\99à¹\88วยรายà¸\81ารà¹\80à¸\9dà¹\89าà¸\94ู (ต้องการจาวาสคริปต์เพื่อเปิดปิดการใช้งาน)",
+ "tog-watchlistunwatchlinks": "à¹\80à¸\9eิà¹\88มà¹\80à¸\84รืà¹\88à¸à¸\87หมายà¹\80ลิà¸\81à¹\80à¸\9dà¹\89าà¸\94ู/à¹\80à¸\9dà¹\89าà¸\94ูà¹\82à¸\94ยà¸\95รà¸\87 ({{int:Watchlist-unwatch}}/{{int:Watchlist-unwatch-undo}}) ลà¸\87à¹\83à¸\99หà¸\99à¹\89าà¸\97ีà¹\88à¹\80à¸\9dà¹\89าà¸\94ูà¸\97ีà¹\88มีà¸\81ารà¹\80à¸\9bลีà¹\88ยà¸\99à¹\81à¸\9bลà¸\87 (ต้องการจาวาสคริปต์เพื่อเปิดปิดการใช้งาน)",
"tog-watchlisthideanons": "ซ่อนการแก้ไขโดยผู้ใช้นิรนามจากรายการเฝ้าดู",
"tog-watchlisthidepatrolled": "ซ่อนการแก้ไขที่ตรวจสอบแล้วจากรายการเฝ้าดู",
"tog-watchlisthidecategorization": "ซ่อนการจัดหมวดหมู่หน้า",
"badarticleerror": "ไม่สามารถดำเนินปฏิบัติการนี้ในหน้านี้",
"cannotdelete": "ไม่สามารถลบหน้าหรือไฟล์ \"$1\" \nผู้อื่นอาจลบไปแล้ว",
"cannotdelete-title": "ไม่สามารถลบหน้า ''$1''",
+ "delete-scheduled": "มีการกำหนดเวลาลบหน้า \"$1\" แล้ว\nโปรดรอสักครู่",
"delete-hook-aborted": "การลบถูกฮุกยกเลิก\nโดยไม่มีคำชี้แจง",
"no-null-revision": "ไม่สามารถสร้างรุ่นแก้ไขว่างใหม่ของหน้า \"$1\"",
"badtitle": "ใช้ชื่อเรื่องนี้ไม่ได้",
"tog-watchdefault": "將我修改嘅頁同檔案加入監視清單",
"tog-watchmoves": "將我移動嘅頁同檔案加入監視清單",
"tog-watchdeletion": "將我刪除嘅頁同檔案加入監視清單",
- "tog-watchuploads": "加入我監視清單入面上載嘅檔案",
+ "tog-watchuploads": "加我上載嘅檔去監視清單度",
"tog-watchrollback": "將我反轉過嘅頁加落監視清單",
"tog-minordefault": "預設全部編輯做細修改",
"tog-previewontop": "喺修改欄上邊顯示預覽",
"listingcontinuesabbrev": "續",
"index-category": "做咗索引嘅版",
"noindex-category": "未做索引嘅版",
- "broken-file-category": "æ\9c\89失æ\95\88æ\96\87件é\8f\88æ\8e¥嘅版",
+ "broken-file-category": "æ\96\87件é\8f\88æ\8e¥å£\9eå\92\97嘅版",
"about": "關於",
"article": "內容頁",
"newwindow": "(響新視窗度打開)",
"trackingcategories-name": "訊息名",
"trackingcategories-desc": "分類收錄標準",
"post-expand-template-inclusion-category-desc": "由於呢篇頁面嘥士喺擴展之前,已經超出咗<code>$wgMaxArticleSize</code>限制,所以好多模都擴展唔到。",
+ "broken-file-category-desc": "呢版有文件鏈接壞咗(即係連去一個唔存在嘅文件)。",
"trackingcategories-nodesc": "冇解說資料",
"trackingcategories-disabled": "類停用咗",
"mailnologin": "冇傳送地址",
"confirmdeletetext": "您正要刪除一個頁面或圖片以及其所有歷史。請確定您要進行此操作,並了解其後果,同時您的行為符合[[{{MediaWiki:Policy-url}}|方針]]。",
"actioncomplete": "操作完成",
"actionfailed": "操作失敗",
- "deletedtext": "已刪除 \"$1\"。\n請參考 $2 檢視最近的刪除記錄。",
+ "deletedtext": "已刪除「$1」。請參考$2檢視最近的刪除記錄。",
"dellogpage": "刪除日誌",
"dellogpagetext": "以下為最近刪除記錄的清單。",
"deletionlog": "刪除日誌",
* Arabic trails too.
*/
$linkTrail = '/^([a-zء-ي]+)(.*)$/sDu';
-
-$imageFiles = [
- 'button-bold' => 'ar/button_bold.png',
- 'button-italic' => 'ar/button_italic.png',
- 'button-link' => 'ar/button_link.png',
- 'button-headline' => 'ar/button_headline.png',
- 'button-nowiki' => 'ar/button_nowiki.png',
-];
$minimumGroupingDigits = 2;
$linkTrail = '/^([абвгґджзеёжзійклмнопрстуўфхцчшыьэюяćčłńśšŭźža-z]+)(.*)$/sDu';
-
-$imageFiles = [
- 'button-bold' => 'be-tarask/button_bold.png',
- 'button-italic' => 'be-tarask/button_italic.png',
- 'button-link' => 'be-tarask/button_link.png',
-];
$separatorTransformTable = [ ',' => '.', '.' => ',' ];
$linkTrail = '/^([äöüßa-z]+)(.*)$/sDu';
-
-$imageFiles = [
- 'button-bold' => 'de/button_bold.png',
- 'button-italic' => 'de/button_italic.png',
-];
*/
$linkPrefixCharset = 'a-zA-Z\\x{80}-\\x{10ffff}';
-/**
- * List of filenames for some ui images that can be overridden per language
- * basis if needed.
- */
-$imageFiles = [
- 'button-bold' => 'en/button_bold.png',
- 'button-italic' => 'en/button_italic.png',
- 'button-link' => 'en/button_link.png',
- 'button-extlink' => 'en/button_extlink.png',
- 'button-headline' => 'en/button_headline.png',
- 'button-image' => 'en/button_image.png',
- 'button-media' => 'en/button_media.png',
- 'button-nowiki' => 'en/button_nowiki.png',
- 'button-sig' => 'en/button_sig.png',
- 'button-hr' => 'en/button_hr.png',
-];
-
/**
* A list of messages to preload for each request.
* Here we add messages that are needed for a typical anonymous parser cache hit.
# Harakat are intentionally not included in the linkTrail. Their addition should
# take place after enough tests.
$linkTrail = "/^([ابپتثجچحخدذرزژسشصضطظعغفقکگلمنوهیآأئؤة]+)(.*)$/sDu";
-
-$imageFiles = [
- 'button-bold' => 'fa/button_bold.png',
- 'button-italic' => 'fa/button_italic.png',
- 'button-link' => 'fa/button_link.png',
- 'button-headline' => 'fa/button_headline.png',
- 'button-nowiki' => 'fa/button_nowiki.png',
-];
'language' => [ '0', '#SHPROOCH:', '#SPROCH:', '#SPRACHE:', '#LANGUAGE:' ],
'hiddencat' => [ '1', '__VERSHTOCHE_SAACHJRUPP__', '__VERSTECKTE_KATEGORIE__', '__WARTUNGSKATEGORIE__', '__HIDDENCAT__' ],
];
-
-$imageFiles = [
- 'button-italic' => 'ksh/button_italic.png',
-];
$fallback8bitEncoding = 'windows-1251';
$linkPrefixExtension = false;
-$imageFiles = [
- 'button-bold' => 'ru/button_bold.png',
- 'button-italic' => 'ru/button_italic.png',
- 'button-link' => 'ru/button_link.png',
-];
-
$linkTrail = '/^([a-zабвгдеёжзийклмнопрстуфхцчшщъыьэюя]+)(.*)$/sDu';
showreviewed
showsizediff
showtoc
-showtoolbar
showunreviewed
shtml
si
"mw.visibleTimeout"
]
},
- {
- "name": "Actions",
- "classes": ["mw.toolbar"]
- },
{
"name": "API",
"classes": ["mw.Api*", "mw.ForeignApi*"]
'dependencies' => 'jquery.cookie',
'targets' => [ 'desktop', 'mobile' ],
],
- 'mediawiki.toolbar' => [
- 'class' => ResourceLoaderEditToolbarModule::class,
- 'scripts' => 'resources/src/mediawiki.toolbar/toolbar.js',
- 'styles' => 'resources/src/mediawiki.toolbar/toolbar.less',
- 'dependencies' => 'jquery.textSelection',
- ],
'mediawiki.experiments' => [
'scripts' => 'resources/src/mediawiki.experiments.js',
'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.block' => [
'scripts' => 'resources/src/mediawiki.special.block.js',
+ 'styles' => 'resources/src/mediawiki.special.block.less',
'dependencies' => [
'oojs-ui-core',
'oojs-ui.styles.icons-editing-core',
'mediawiki.htmlform',
'moment',
],
+ 'targets' => [ 'desktop', 'mobile' ],
],
'mediawiki.special.changecredentials.js' => [
'scripts' => 'resources/src/mediawiki.special.changecredentials.js',
],
'targets' => [ 'desktop', 'mobile' ],
],
+ 'mediawiki.widgets.TitlesMultiselectWidget' => [
+ 'scripts' => [
+ 'resources/src/mediawiki.widgets/mw.widgets.TitlesMultiselectWidget.js',
+ ],
+ 'dependencies' => [
+ 'mediawiki.api',
+ 'oojs-ui-widgets',
+ // FIXME: Needs TitleInputWidget only
+ 'mediawiki.widgets',
+ ],
+ 'targets' => [ 'desktop', 'mobile' ],
+ ],
'mediawiki.widgets.SearchInputWidget' => [
'scripts' => [
'resources/src/mediawiki.widgets/mw.widgets.SearchInputWidget.js',
list-style: none;
}
}
-
- & > div {
- margin-right: @margin-circle-result;
- }
}
// Make more specific for the overrides
enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ),
hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ),
watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ),
- expiryWidget = infuseOrNull( 'mw-input-wpExpiry' );
+ expiryWidget = infuseOrNull( 'mw-input-wpExpiry' ),
+ editingRestrictionWidget = infuseOrNull( 'mw-input-wpEditingRestriction' ),
+ pageRestrictionsWidget = infuseOrNull( 'mw-input-wpPageRestrictions' );
function updateBlockOptions() {
var blocktarget = blockTargetWidget.getValue().trim(),
expiryValue = expiryWidget.getValue(),
// infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity)
infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ],
- isIndefinite = infinityValues.indexOf( expiryValue ) !== -1;
+ isIndefinite = infinityValues.indexOf( expiryValue ) !== -1,
+ editingRestrictionValue = editingRestrictionWidget ? editingRestrictionWidget.getValue() : undefined;
if ( enableAutoblockField ) {
enableAutoblockField.toggle( !( isNonEmptyIp ) );
if ( watchUserField ) {
watchUserField.toggle( !( isIpRange && !isEmpty ) );
}
+ if ( pageRestrictionsWidget ) {
+ pageRestrictionsWidget.setDisabled( editingRestrictionValue === 'sitewide' );
+ }
}
if ( blockTargetWidget ) {
// Bind functions so they're checked whenever stuff changes
blockTargetWidget.on( 'change', updateBlockOptions );
expiryWidget.on( 'change', updateBlockOptions );
+ editingRestrictionWidget.on( 'change', updateBlockOptions );
// Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours)
updateBlockOptions();
--- /dev/null
+.mw-block-page-restrictions {
+ margin-left: 2em;
+ .oo-ui-widget {
+ max-width: 48em;
+ }
+}
+++ /dev/null
-
-button_italic.png
--------------------
-Source : https://commons.wikimedia.org/wiki/Image:Button_S_italic.png
-License: Public domain
-Author : Purodha Blissenbach, https://ksh.wikipedia.org/wiki/User:Purodha
-
+++ /dev/null
-button_bold.png
----------------
-Source : https://commons.wikimedia.org/wiki/File:Button_bold_ukr.png
-License: Public domain
-Author : Alexey Belomoev
-
-button_italic.png
-------------------------
-Source : https://commons.wikimedia.org/wiki/File:Button_italic_ukr.png
-License: Public domain
-Author : Alexey Belomoev
-
-button_link.png
------------------
-Source : https://commons.wikimedia.org/wiki/File:Button_internal_link_ukr.png
-License: GPL
-Author : Saproj, Erik Möller
+++ /dev/null
-/**
- * Interface for the classic edit toolbar.
- *
- * @class mw.toolbar
- * @singleton
- */
-( function () {
- var toolbar, isReady, $toolbar, queue, slice, $currentFocused;
-
- /**
- * Internal helper that does the actual insertion of the button into the toolbar.
- *
- * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
- * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
- *
- * @private
- *
- * @param {Object} button Object with the following properties.
- * You are required to provide *either* the `onClick` parameter, or the three parameters
- * `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
- * @param {string} [button.imageFile] Image to use for the button.
- * @param {string} button.speedTip Tooltip displayed when user mouses over the button.
- * @param {Function} [button.onClick] Function to be executed when the button is clicked.
- * @param {string} [button.tagOpen]
- * @param {string} [button.tagClose]
- * @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
- * `sampleText` together provide the markup that should be inserted into page text at
- * current cursor position.
- * @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
- * used to define the image with CSS if it's not provided as `imageFile`.
- * @param {string} [speedTip]
- * @param {string} [tagOpen]
- * @param {string} [tagClose]
- * @param {string} [sampleText]
- * @param {string} [imageId]
- */
- function insertButton( button, speedTip, tagOpen, tagClose, sampleText, imageId ) {
- var $button;
-
- // Backwards compatibility
- if ( typeof button !== 'object' ) {
- button = {
- imageFile: button,
- speedTip: speedTip,
- tagOpen: tagOpen,
- tagClose: tagClose,
- sampleText: sampleText,
- imageId: imageId
- };
- }
-
- if ( button.imageFile ) {
- $button = $( '<img>' ).attr( {
- src: button.imageFile,
- alt: button.speedTip,
- title: button.speedTip,
- id: button.imageId || undefined,
- 'class': 'mw-toolbar-editbutton'
- } );
- } else {
- $button = $( '<div>' ).attr( {
- title: button.speedTip,
- id: button.imageId || undefined,
- 'class': 'mw-toolbar-editbutton'
- } );
- }
-
- $button.click( function ( e ) {
- if ( button.onClick !== undefined ) {
- button.onClick( e );
- } else {
- toolbar.insertTags( button.tagOpen, button.tagClose, button.sampleText );
- }
-
- return false;
- } );
-
- $toolbar.append( $button );
- }
-
- isReady = false;
- $toolbar = false;
-
- /**
- * @private
- * @property {Array}
- * Contains button objects (and for backwards compatibility, it can
- * also contains an arguments array for insertButton).
- */
- queue = [];
- slice = queue.slice;
-
- toolbar = {
-
- /**
- * Add buttons to the toolbar.
- *
- * Takes care of race conditions and time-based dependencies by placing buttons in a queue if
- * this method is called before the toolbar is created.
- *
- * For backwards-compatibility, passing `imageFile`, `speedTip`, `tagOpen`, `tagClose`,
- * `sampleText` and `imageId` as separate arguments (in this order) is also supported.
- *
- * @inheritdoc #insertButton
- */
- addButton: function () {
- if ( isReady ) {
- insertButton.apply( toolbar, arguments );
- } else {
- // Convert arguments list to array
- queue.push( slice.call( arguments ) );
- }
- },
-
- /**
- * Add multiple buttons to the toolbar (see also #addButton).
- *
- * Example usage:
- *
- * addButtons( [ { .. }, { .. }, { .. } ] );
- * addButtons( { .. }, { .. } );
- *
- * @param {...Object|Array} [buttons] An array of button objects or the first
- * button object in a list of variadic arguments.
- */
- addButtons: function ( buttons ) {
- if ( !Array.isArray( buttons ) ) {
- buttons = slice.call( arguments );
- }
- if ( isReady ) {
- buttons.forEach( function ( button ) {
- insertButton( button );
- } );
- } else {
- // Push each button into the queue
- queue.push.apply( queue, buttons );
- }
- },
-
- /**
- * Apply tagOpen/tagClose to selection in currently focused textarea.
- *
- * Uses `sampleText` if selection is empty.
- *
- * @param {string} tagOpen
- * @param {string} tagClose
- * @param {string} sampleText
- */
- insertTags: function ( tagOpen, tagClose, sampleText ) {
- if ( $currentFocused && $currentFocused.length ) {
- $currentFocused.textSelection(
- 'encapsulateSelection', {
- pre: tagOpen,
- peri: sampleText,
- post: tagClose
- }
- );
- }
- }
- };
-
- // Legacy (for compatibility with the code previously in skins/common.edit.js)
- mw.log.deprecate( window, 'addButton', toolbar.addButton, 'Use mw.toolbar.addButton instead.' );
- mw.log.deprecate( window, 'insertTags', toolbar.insertTags, 'Use mw.toolbar.insertTags instead.' );
-
- // For backwards compatibility. Used to be called from EditPage.php, maybe other places as well.
- toolbar.init = $.noop;
-
- // Expose API publicly
- // @deprecated since MW 1.29
- mw.log.deprecate( mw, 'toolbar', toolbar, null, 'mw.toolbar' );
-
- $( function () {
- var i, button;
-
- // Used to determine where to insert tags
- $currentFocused = $( '#wpTextbox1' );
-
- // Populate the selector cache for $toolbar
- $toolbar = $( '#toolbar' );
-
- for ( i = 0; i < queue.length; i++ ) {
- button = queue[ i ];
- if ( Array.isArray( button ) ) {
- // Forwarded arguments array from mw.toolbar.addButton
- insertButton.apply( toolbar, button );
- } else {
- // Raw object from mw.toolbar.addButtons
- insertButton( button );
- }
- }
-
- // Clear queue
- queue.length = 0;
-
- // This causes further calls to addButton to go to insertion directly
- // instead of to the queue.
- // It is important that this is after the one and only loop through
- // the queue
- isReady = true;
-
- // Apply to dynamically created textboxes as well as normal ones
- $( document ).on( 'focus', 'textarea, input:text', function () {
- $currentFocused = $( this );
- } );
- } );
-
-}() );
+++ /dev/null
-@import 'mediawiki.mixins';
-
-#mw-editbutton-bold {
- .background-image('images/@{button-bold}');
-}
-
-#mw-editbutton-italic {
- .background-image('images/@{button-italic}');
-}
-
-#mw-editbutton-link {
- .background-image('images/@{button-link}');
-}
-
-#mw-editbutton-extlink {
- .background-image('images/@{button-extlink}');
-}
-
-#mw-editbutton-headline {
- .background-image('images/@{button-headline}');
-}
-
-#mw-editbutton-image {
- .background-image('images/@{button-image}');
-}
-
-#mw-editbutton-media {
- .background-image('images/@{button-media}');
-}
-
-#mw-editbutton-nowiki {
- .background-image('images/@{button-nowiki}');
-}
-
-// Who decided to make only this single one different than the name of the data item?
-#mw-editbutton-signature {
- .background-image('images/@{button-sig}');
-}
-
-#mw-editbutton-hr {
- .background-image('images/@{button-hr}');
-}
* @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
* @cfg {boolean} [validateTitle=true] Whether the input must be a valid title
* @cfg {boolean} [required=false] Whether the input must not be empty
+ * @cfg {boolean} [highlightSearchQuery=true] Highlight the partial query the user used for this title
* @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
* @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified
*/
this.addQueryInput = config.addQueryInput !== false;
this.excludeCurrentPage = !!config.excludeCurrentPage;
this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
+ this.highlightSearchQuery = config.highlightSearchQuery === undefined ? true : !!config.highlightSearchQuery;
this.cache = config.cache;
this.api = config.api || new mw.Api();
// Supports: IE10, FF28, Chrome23
missing: data.missing,
redirect: data.redirect,
disambiguation: data.disambiguation,
- query: this.getQueryValue(),
+ query: this.highlightSearchQuery ? this.getQueryValue() : null,
compare: this.compare
};
};
--- /dev/null
+/*!
+ * MediaWiki Widgets - TitlesMultiselectWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function () {
+
+ /**
+ * Creates an mw.widgets.TitlesMultiselectWidget object
+ *
+ * @class
+ * @extends OO.ui.MenuTagMultiselectWidget
+ * @mixins OO.ui.mixin.RequestManager
+ * @mixins OO.ui.mixin.PendingElement
+ * @mixins mw.widgets.TitleWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ mw.widgets.TitlesMultiselectWidget = function MwWidgetsTitlesMultiselectWidget( config ) {
+ config = $.extend( true, {
+ // Shouldn't this be handled by MenuTagMultiselectWidget?
+ options: config.selected ? config.selected.map( function ( title ) {
+ return {
+ data: title,
+ label: title
+ };
+ } ) : []
+ }, config );
+
+ // Parent constructor
+ mw.widgets.TitlesMultiselectWidget.parent.call( this, $.extend( true,
+ {
+ clearInputOnChoose: true,
+ inputPosition: 'inline',
+ allowEditTags: false
+ },
+ config
+ ) );
+
+ // Mixin constructors
+ mw.widgets.TitleWidget.call( this, $.extend( true, {
+ addQueryInput: true,
+ highlightSearchQuery: false
+ }, config ) );
+ OO.ui.mixin.RequestManager.call( this, config );
+ OO.ui.mixin.PendingElement.call( this, $.extend( true, {}, config, {
+ $pending: this.$handle
+ } ) );
+
+ // Validate from mw.widgets.TitleWidget
+ this.input.setValidation( this.isQueryValid.bind( this ) );
+
+ if ( this.maxLength !== undefined ) {
+ // maxLength is defined through TitleWidget parent
+ this.input.$input.attr( 'maxlength', this.maxLength );
+ }
+
+ // Initialization
+ this.$element
+ .addClass( 'mw-widgets-titlesMultiselectWidget' );
+
+ this.menu.$element
+ // For consistency, use the same classes as TitleWidget
+ // expects for menu results
+ .addClass( 'mw-widget-titleWidget-menu' )
+ .toggleClass( 'mw-widget-titleWidget-menu-withImages', this.showImages )
+ .toggleClass( 'mw-widget-titleWidget-menu-withDescriptions', this.showDescriptions );
+
+ if ( 'name' in config ) {
+ // Use this instead of <input type="hidden">, because hidden inputs do not have separate
+ // 'value' and 'defaultValue' properties. The script on Special:Preferences
+ // (mw.special.preferences.confirmClose) checks this property to see if a field was changed.
+ this.hiddenInput = $( '<textarea>' )
+ .addClass( 'oo-ui-element-hidden' )
+ .attr( 'name', config.name )
+ .appendTo( this.$element );
+ // Update with preset values
+ // Set the default value (it might be different from just being empty)
+ this.hiddenInput.prop( 'defaultValue', this.getItems().map( function ( item ) {
+ return item.getData();
+ } ).join( '\n' ) );
+ this.on( 'change', function ( items ) {
+ this.hiddenInput.val( items.map( function ( item ) {
+ return item.getData();
+ } ).join( '\n' ) );
+ // Trigger a 'change' event as if a user edited the text
+ // (it is not triggered when changing the value from JS code).
+ this.hiddenInput.trigger( 'change' );
+ }.bind( this ) );
+ }
+
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.widgets.TitlesMultiselectWidget, OO.ui.MenuTagMultiselectWidget );
+ OO.mixinClass( mw.widgets.TitlesMultiselectWidget, OO.ui.mixin.RequestManager );
+ OO.mixinClass( mw.widgets.TitlesMultiselectWidget, OO.ui.mixin.PendingElement );
+ OO.mixinClass( mw.widgets.TitlesMultiselectWidget, mw.widgets.TitleWidget );
+
+ /* Methods */
+
+ mw.widgets.TitlesMultiselectWidget.prototype.getQueryValue = function () {
+ return this.input.getValue();
+ };
+
+ /**
+ * @inheritdoc OO.ui.MenuTagMultiselectWidget
+ */
+ mw.widgets.TitlesMultiselectWidget.prototype.onInputChange = function () {
+ var widget = this;
+
+ this.getRequestData()
+ .then( function ( data ) {
+ // Reset
+ widget.menu.clearItems();
+ widget.menu.addItems( widget.getOptionsFromData( data ) );
+ } );
+
+ mw.widgets.TitlesMultiselectWidget.parent.prototype.onInputChange.call( this );
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+ mw.widgets.TitlesMultiselectWidget.prototype.getRequestQuery = function () {
+ return this.getQueryValue();
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+ mw.widgets.TitlesMultiselectWidget.prototype.getRequest = function () {
+ return this.getSuggestionsPromise();
+ };
+
+ /**
+ * @inheritdoc OO.ui.mixin.RequestManager
+ */
+ mw.widgets.TitlesMultiselectWidget.prototype.getRequestCacheDataFromResponse = function ( response ) {
+ return response.query || {};
+ };
+}() );
'MediaWiki\\Auth\\AuthenticationRequestTestCase' =>
"$testDir/phpunit/includes/auth/AuthenticationRequestTestCase.php",
+ # tests/phpunit/includes/block
+ 'MediaWiki\\Tests\\Block\\Restriction\\RestrictionTestCase' => "$testDir/phpunit/includes/block/Restriction/RestrictionTestCase.php",
+
# tests/phpunit/includes/changes
'TestRecentChangesHelper' => "$testDir/phpunit/includes/changes/TestRecentChangesHelper.php",
<?php
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+
/**
* @group Database
* @group Blocking
}
}
+ /**
+ * @covers Block::newFromRow
+ */
+ public function testNewFromRow() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ ] );
+ $block->insert();
+
+ $blockQuery = Block::getQueryInfo();
+ $row = $this->db->select(
+ $blockQuery['tables'],
+ $blockQuery['fields'],
+ [
+ 'ipb_id' => $block->getId(),
+ ],
+ __METHOD__,
+ [],
+ $blockQuery['joins']
+ )->fetchObject();
+
+ $block = Block::newFromRow( $row );
+ $this->assertInstanceOf( Block::class, $block );
+ $this->assertEquals( $block->getBy(), $sysop->getId() );
+ $this->assertEquals( $block->getTarget()->getName(), $badActor->getName() );
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::equals
+ */
+ public function testEquals() {
+ $block = new Block();
+
+ $this->assertTrue( $block->equals( $block ) );
+
+ $partial = new Block( [
+ 'sitewide' => false,
+ ] );
+ $this->assertFalse( $block->equals( $partial ) );
+ }
+
+ /**
+ * @covers Block::isSitewide
+ */
+ public function testIsSitewide() {
+ $block = new Block();
+ $this->assertTrue( $block->isSitewide() );
+
+ $block = new Block( [
+ 'sitewide' => true,
+ ] );
+ $this->assertTrue( $block->isSitewide() );
+
+ $block = new Block( [
+ 'sitewide' => false,
+ ] );
+ $this->assertFalse( $block->isSitewide() );
+
+ $block = new Block( [
+ 'sitewide' => false,
+ ] );
+ $block->isSitewide( true );
+ $this->assertTrue( $block->isSitewide() );
+ }
+
+ /**
+ * @covers Block::getRestrictions
+ * @covers Block::setRestrictions
+ */
+ public function testRestrictions() {
+ $block = new Block();
+ $restrictions = [
+ new PageRestriction( 0, 1 )
+ ];
+ $block->setRestrictions( $restrictions );
+
+ $this->assertSame( $restrictions, $block->getRestrictions() );
+ }
+
+ /**
+ * @covers Block::getRestrictions
+ * @covers Block::insert
+ */
+ public function testRestrictionsFromDatabase() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ ] );
+ $page = $this->getExistingTestPage( 'Foo' );
+ $restriction = new PageRestriction( 0, $page->getId() );
+ $block->setRestrictions( [ $restriction ] );
+ $block->insert();
+
+ // Refresh the block from the database.
+ $block = Block::newFromID( $block->getId() );
+ $restrictions = $block->getRestrictions();
+ $this->assertCount( 1, $restrictions );
+ $this->assertTrue( $restriction->equals( $restrictions[0] ) );
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::insert
+ */
+ public function testInsertExistingBlock() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ ] );
+ $page = $this->getExistingTestPage( 'Foo' );
+ $restriction = new PageRestriction( 0, $page->getId() );
+ $block->setRestrictions( [ $restriction ] );
+ $block->insert();
+
+ // Insert the block again, which should result in a failur
+ $result = $block->insert();
+
+ $this->assertFalse( $result );
+
+ // Ensure that there are no restrictions where the blockId is 0.
+ $count = $this->db->selectRowCount(
+ 'ipblocks_restrictions',
+ '*',
+ [ 'ir_ipb_id' => 0 ],
+ __METHOD__
+ );
+ $this->assertSame( 0, $count );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::preventsEdit
+ */
+ public function testPreventsEditReturnsTrueOnSitewideBlock() {
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => true,
+ 'sitewide' => true
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $title = $this->getExistingTestPage( 'Foo' )->getTitle();
+
+ $this->assertTrue( $block->preventsEdit( $title ) );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::preventsEdit
+ */
+ public function testPreventsEditOnPartialBlock() {
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => true,
+ 'sitewide' => false
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+
+ $pageRestriction = new PageRestriction( $block->getId(), $pageFoo->getId() );
+ BlockRestriction::insert( [ $pageRestriction ] );
+
+ $this->assertTrue( $block->preventsEdit( $pageFoo->getTitle() ) );
+ $this->assertFalse( $block->preventsEdit( $pageBar->getTitle() ) );
+
+ $block->delete();
+ }
+
+ /**
+ * @covers Block::preventsEdit
+ * @dataProvider preventsEditOnUserTalkProvider
+ */
+ public function testPreventsEditOnUserTalkPage(
+ $allowUsertalk, $sitewide, $result, $blockAllowsUTEdit = true
+ ) {
+ $this->setMwGlobals( [
+ 'wgBlockAllowsUTEdit' => $blockAllowsUTEdit,
+ ] );
+
+ $user = $this->getTestUser()->getUser();
+ $block = new Block( [
+ 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
+ 'allowUsertalk' => $allowUsertalk,
+ 'sitewide' => $sitewide
+ ] );
+
+ $block->setTarget( $user );
+ $block->setBlocker( $this->getTestSysop()->getUser() );
+ $block->insert();
+
+ $this->assertEquals( $result, $block->preventsEdit( $user->getTalkPage() ) );
+ $block->delete();
+ }
+
+ public function preventsEditOnUserTalkProvider() {
+ return [
+ [
+ 'allowUsertalk' => false,
+ 'sitewide' => true,
+ 'result' => true,
+ ],
+ [
+ 'allowUsertalk' => true,
+ 'sitewide' => true,
+ 'result' => false,
+ ],
+ [
+ 'allowUsertalk' => true,
+ 'sitewide' => false,
+ 'result' => false,
+ ],
+ [
+ 'allowUsertalk' => false,
+ 'sitewide' => false,
+ 'result' => true,
+ ],
+ [
+ 'allowUsertalk' => true,
+ 'sitewide' => true,
+ 'result' => true,
+ 'blockAllowsUTEdit' => false
+ ],
+ [
+ 'allowUsertalk' => true,
+ 'sitewide' => false,
+ 'result' => true,
+ 'blockAllowsUTEdit' => false
+ ],
+ ];
+ }
}
* @group MediaWiki
*/
class MediaWikiServicesTest extends MediaWikiTestCase {
+ private $deprecatedServices = [ 'CryptRand' ];
/**
* @return Config
$getterCases[$name] = [
'get' . $service,
$class,
+ in_array( $service, $this->deprecatedServices )
];
}
/**
* @dataProvider provideGetters
*/
- public function testGetters( $getter, $type ) {
+ public function testGetters( $getter, $type, $isDeprecated = false ) {
+ if ( $isDeprecated ) {
+ $this->hideDeprecated( MediaWikiServices::class . "::$getter" );
+ }
+
// Test against the default instance, since the dummy will not know the default services.
$services = MediaWikiServices::getInstance();
$service = $services->$getter();
$op = $this->newInstance();
$this->assertSame( '', $op->getHTML() );
+ $this->hideDeprecated( 'OutputPage::addWikiText' );
$this->hideDeprecated( 'OutputPage::addWikiTextTitle' );
$this->hideDeprecated( 'OutputPage::addWikiTextWithTitle' );
$this->hideDeprecated( 'OutputPage::addWikiTextTidy' );
$this->hideDeprecated( 'OutputPage::addWikiTextTitleTidy' );
+ $this->hideDeprecated( 'disabling tidy' );
+
if ( in_array(
$method,
[ 'addWikiTextWithTitle', 'addWikiTextTitleTidy', 'addWikiTextTitle' ]
* @covers OutputPage::addWikiText
*/
public function testAddWikiTextNoTitle() {
+ $this->hideDeprecated( 'OutputPage::addWikiText' );
$this->setExpectedException( MWException::class, 'Title is null' );
$op = $this->newInstance( [], null, 'notitle' );
$op = $this->newInstance();
$this->assertSame( '', $op->getHTML() );
$op->addWikiMsg( 'parentheses', "<b>a" );
- // This is known to be bad unbalanced HTML; this will be fixed
- // by I743f4185a03403f8d9b9db010ff1ee4e9342e062 (T198214)
- $this->assertSame( "<p>(<b>a)\n</p>", $op->getHTML() );
+ // The input is bad unbalanced HTML, but the output is tidied
+ $this->assertSame( "<p>(<b>a)\n</b></p>", $op->getHTML() );
}
/**
'Useruser', 'test', '23:00, 31 December 1969', '127.0.8.1',
$wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
$this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
+
+ // partial block message test
+ $this->user->mBlockedby = $this->user->getName();
+ $this->user->mBlock = new Block( [
+ 'address' => '127.0.8.1',
+ 'by' => $this->user->getId(),
+ 'reason' => 'no reason given',
+ 'timestamp' => $now,
+ 'sitewide' => false,
+ 'expiry' => 10,
+ ] );
+
+ $this->assertEquals( [ [ 'blockedtext-partial',
+ '[[User:Useruser|Useruser]]', 'no reason given', '127.0.0.1',
+ 'Useruser', null, '23:00, 31 December 1969', '127.0.8.1',
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $now ), true ) ] ],
+ $this->title->getUserPermissionsErrors( 'move-target', $this->user ) );
}
}
$this->doBlock( [ 'expiry' => '' ] );
}
+ public function testBlockWithRestrictions() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+
+ $title = 'Foo';
+ $page = $this->getExistingTestPage( $title );
+
+ $this->doBlock( [
+ 'partial' => true,
+ 'pagerestrictions' => $title,
+ ] );
+
+ $block = Block::newFromTarget( $this->mUser->getName() );
+
+ $this->assertFalse( $block->isSitewide() );
+ $this->assertCount( 1, $block->getRestrictions() );
+ $this->assertEquals( $title, $block->getRestrictions()[0]->getTitle()->getText() );
+ }
+
/**
* @expectedException ApiUsageException
* @expectedExceptionMessage The "token" parameter must be set
self::$users['sysop']->getUser()
);
}
+
+ /**
+ * @expectedException ApiUsageException
+ * @expectedExceptionMessage Invalid value "127.0.0.1/64" for user parameter "user".
+ */
+ public function testBlockWithLargeRange() {
+ $tokens = $this->getTokens();
+
+ $this->doApiRequest(
+ [
+ 'action' => 'block',
+ 'user' => '127.0.0.1/64',
+ 'reason' => 'Some reason',
+ 'token' => $tokens['blocktoken'],
+ ],
+ null,
+ false,
+ self::$users['sysop']->getUser()
+ );
+ }
+
+ /**
+ * @expectedException ApiUsageException
+ * @expectedExceptionMessage "pagerestrictions" may not be over 10 (set to 11) for bots or sysops.
+ */
+ public function testBlockingToManyRestrictions() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+
+ $tokens = $this->getTokens();
+
+ $this->doApiRequest(
+ [
+ 'action' => 'block',
+ 'user' => $this->mUser->getName(),
+ 'reason' => 'Some reason',
+ 'partial' => true,
+ 'pagerestrictions' => 'One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven',
+ 'token' => $tokens['blocktoken'],
+ ],
+ null,
+ false,
+ self::$users['sysop']->getUser()
+ );
+ }
}
--- /dev/null
+<?php
+
+use MediaWiki\Block\Restriction\PageRestriction;
+
+/**
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiQueryBlocks
+ */
+class ApiQueryBlocksTest extends ApiTestCase {
+
+ protected $tablesUsed = [
+ 'ipblocks',
+ 'ipblocks_restrictions',
+ ];
+
+ public function testExecute() {
+ list( $data ) = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'blocks',
+ ] );
+ $this->assertEquals( [ 'batchcomplete' => true, 'query' => [ 'blocks' => [] ] ], $data );
+ }
+
+ public function testExecuteBlock() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ ] );
+
+ $block->insert();
+
+ list( $data ) = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'blocks',
+ ] );
+ $this->arrayHasKey( 'query', $data );
+ $this->arrayHasKey( 'blocks', $data['query'] );
+ $this->assertCount( 1, $data['query']['blocks'] );
+ $subset = [
+ 'id' => $block->getId(),
+ 'user' => $badActor->getName(),
+ 'expiry' => $block->getExpiry(),
+ ];
+ $this->assertArraySubset( $subset, $data['query']['blocks'][0] );
+ }
+
+ public function testExecuteSitewide() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'ipb_expiry' => 'infinity',
+ 'ipb_sitewide' => 1,
+ ] );
+
+ $block->insert();
+
+ list( $data ) = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'blocks',
+ ] );
+ $this->arrayHasKey( 'query', $data );
+ $this->arrayHasKey( 'blocks', $data['query'] );
+ $this->assertCount( 1, $data['query']['blocks'] );
+ $subset = [
+ 'id' => $block->getId(),
+ 'user' => $badActor->getName(),
+ 'expiry' => $block->getExpiry(),
+ 'partial' => !$block->isSitewide(),
+ ];
+ $this->assertArraySubset( $subset, $data['query']['blocks'][0] );
+ }
+
+ public function testExecuteRestrictions() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ 'sitewide' => 0,
+ ] );
+
+ $block->insert();
+
+ $subset = [
+ 'id' => $block->getId(),
+ 'user' => $badActor->getName(),
+ 'expiry' => $block->getExpiry(),
+ ];
+
+ $title = 'Lady Macbeth';
+ $pageData = $this->insertPage( $title );
+ $pageId = $pageData['id'];
+
+ $this->db->insert( 'ipblocks_restrictions', [
+ 'ir_ipb_id' => $block->getId(),
+ 'ir_type' => PageRestriction::TYPE_ID,
+ 'ir_value' => $pageId,
+ ] );
+ $this->db->insert( 'ipblocks_restrictions', [
+ 'ir_ipb_id' => $block->getId(),
+ 'ir_type' => 2,
+ 'ir_value' => 3,
+ ] );
+
+ // Test without requesting restrictions.
+ list( $data ) = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'blocks',
+ ] );
+ $this->arrayHasKey( 'query', $data );
+ $this->arrayHasKey( 'blocks', $data['query'] );
+ $this->assertCount( 1, $data['query']['blocks'] );
+ $flagSubset = array_merge( $subset, [
+ 'partial' => !$block->isSitewide(),
+ ] );
+ $this->assertArraySubset( $flagSubset, $data['query']['blocks'][0] );
+ $this->assertArrayNotHasKey( 'restrictions', $data['query']['blocks'][0] );
+
+ // Test requesting the restrictions.
+ list( $data ) = $this->doApiRequest( [
+ 'action' => 'query',
+ 'list' => 'blocks',
+ 'bkprop' => 'id|user|expiry|restrictions'
+ ] );
+ $this->arrayHasKey( 'query', $data );
+ $this->arrayHasKey( 'blocks', $data['query'] );
+ $this->assertCount( 1, $data['query']['blocks'] );
+ $restrictionsSubset = array_merge( $subset, [
+ 'restrictions' => [
+ 'pages' => [
+ [
+ 'id' => $pageId,
+ 'ns' => 0,
+ 'title' => $title,
+ ],
+ ],
+ ],
+ ] );
+ $this->assertArraySubset( $restrictionsSubset, $data['query']['blocks'][0] );
+ $this->assertArrayNotHasKey( 'partial', $data['query']['blocks'][0] );
+ }
+}
'user' => $badActor->getId(),
'by' => $sysop->getId(),
'expiry' => 'infinity',
- 'sitewide' => 0,
+ 'sitewide' => 1,
'enableAutoblock' => true,
] );
--- /dev/null
+<?php
+
+/**
+ * @group medium
+ * @covers ApiQueryUserInfo
+ */
+class ApiQueryUserInfoTest extends ApiTestCase {
+ public function testGetBlockInfo() {
+ $apiQueryUserInfo = new ApiQueryUserInfo(
+ new ApiQuery( new ApiMain( $this->apiContext ), 'userinfo' ),
+ 'userinfo'
+ );
+
+ $block = new Block();
+ $info = $apiQueryUserInfo->getBlockInfo( $block );
+ $subset = [
+ 'blockid' => null,
+ 'blockedby' => '',
+ 'blockedbyid' => 0,
+ 'blockreason' => '',
+ 'blockexpiry' => 'infinite',
+ 'blockpartial' => false,
+ ];
+ $this->assertArraySubset( $subset, $info );
+ }
+
+ public function testGetBlockInfoPartial() {
+ $apiQueryUserInfo = new ApiQueryUserInfo(
+ new ApiQuery( new ApiMain( $this->apiContext ), 'userinfo' ),
+ 'userinfo'
+ );
+
+ $block = new Block( [
+ 'sitewide' => false,
+ ] );
+ $info = $apiQueryUserInfo->getBlockInfo( $block );
+ $subset = [
+ 'blockid' => null,
+ 'blockedby' => '',
+ 'blockedbyid' => 0,
+ 'blockreason' => '',
+ 'blockexpiry' => 'infinite',
+ 'blockpartial' => true,
+ ];
+ $this->assertArraySubset( $subset, $info );
+ }
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Block;
+
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\Block\Restriction\Restriction;
+
+/**
+ * @group Database
+ * @group Blocking
+ * @coversDefaultClass \MediaWiki\Block\BlockRestriction
+ */
+class BlockRestrictionTest extends \MediaWikiLangTestCase {
+
+ public function tearDown() {
+ parent::tearDown();
+ $this->resetTables();
+ }
+
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testLoadMultipleRestrictions() {
+ $block = $this->insertBlock();
+
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ ] );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+
+ $this->assertCount( 2, $restrictions );
+ }
+
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testWithNoRestrictions() {
+ $block = $this->insertBlock();
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertEmpty( $restrictions );
+ }
+
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testWithEmptyParam() {
+ $restrictions = BlockRestriction::loadByBlockId( [] );
+
+ $this->assertEmpty( $restrictions );
+ }
+
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testIgnoreNotSupportedTypes() {
+ $block = $this->insertBlock();
+
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+
+ // valid type
+ $this->insertRestriction( $block->getId(), PageRestriction::TYPE_ID, $pageFoo->getId() );
+
+ // invalid type
+ $this->insertRestriction( $block->getId(), 9, $pageBar->getId() );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+ }
+
+ /**
+ * @covers ::loadByBlockId
+ * @covers ::resultToRestrictions
+ * @covers ::rowToRestriction
+ */
+ public function testMappingRestrictionObject() {
+ $block = $this->insertBlock();
+ $title = 'Lady Macbeth';
+ $page = $this->getExistingTestPage( $title );
+
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+
+ list( $pageRestriction ) = $restrictions;
+ $this->assertInstanceOf( PageRestriction::class, $pageRestriction );
+ $this->assertEquals( $block->getId(), $pageRestriction->getBlockId() );
+ $this->assertEquals( $page->getId(), $pageRestriction->getValue() );
+ $this->assertEquals( $pageRestriction->getType(), PageRestriction::TYPE );
+ $this->assertEquals( $pageRestriction->getTitle()->getText(), $title );
+ }
+
+ /**
+ * @covers ::insert
+ */
+ public function testInsert() {
+ $block = $this->insertBlock();
+
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+
+ $restrictions = [
+ new \stdClass(),
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ ];
+
+ $result = BlockRestriction::insert( $restrictions );
+ $this->assertTrue( $result );
+
+ $restrictions = [
+ new \stdClass(),
+ ];
+
+ $result = BlockRestriction::insert( $restrictions );
+ $this->assertFalse( $result );
+
+ $result = BlockRestriction::insert( [] );
+ $this->assertFalse( $result );
+ }
+
+ /**
+ * @covers ::insert
+ */
+ public function testInsertTypes() {
+ $block = $this->insertBlock();
+
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+
+ $namespace = $this->createMock( Restriction::class );
+ $namespace->method( 'toRow' )
+ ->willReturn( [
+ 'ir_ipb_id' => $block->getId(),
+ 'ir_type' => 2,
+ 'ir_value' => 0,
+ ] );
+
+ $invalid = $this->createMock( Restriction::class );
+ $invalid->method( 'toRow' )
+ ->willReturn( [
+ 'ir_ipb_id' => $block->getId(),
+ 'ir_type' => 9,
+ 'ir_value' => 42,
+ ] );
+
+ $restrictions = [
+ new \stdClass(),
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ $namespace,
+ $invalid,
+ ];
+
+ $result = BlockRestriction::insert( $restrictions );
+ $this->assertTrue( $result );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 2, $restrictions );
+ }
+
+ /**
+ * @covers ::update
+ * @covers ::restrictionsByBlockId
+ * @covers ::restrictionsToRemove
+ */
+ public function testUpdateInsert() {
+ $block = $this->insertBlock();
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ ] );
+
+ BlockRestriction::update( [
+ new \stdClass(),
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ ] );
+
+ $db = wfGetDb( DB_REPLICA );
+ $result = $db->select(
+ [ 'ipblocks_restrictions' ],
+ [ '*' ],
+ [ 'ir_ipb_id' => $block->getId() ]
+ );
+
+ $this->assertEquals( 1, $result->numRows() );
+ $row = $result->fetchObject();
+ $this->assertEquals( $block->getId(), $row->ir_ipb_id );
+ $this->assertEquals( $pageBar->getId(), $row->ir_value );
+ }
+
+ /**
+ * @covers ::update
+ * @covers ::restrictionsByBlockId
+ * @covers ::restrictionsToRemove
+ */
+ public function testUpdateChange() {
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+
+ BlockRestriction::update( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ $db = wfGetDb( DB_REPLICA );
+ $result = $db->select(
+ [ 'ipblocks_restrictions' ],
+ [ '*' ],
+ [ 'ir_ipb_id' => $block->getId() ]
+ );
+
+ $this->assertEquals( 1, $result->numRows() );
+ $row = $result->fetchObject();
+ $this->assertEquals( $block->getId(), $row->ir_ipb_id );
+ $this->assertEquals( $page->getId(), $row->ir_value );
+ }
+
+ /**
+ * @covers ::update
+ * @covers ::restrictionsByBlockId
+ * @covers ::restrictionsToRemove
+ */
+ public function testUpdateNoRestrictions() {
+ $block = $this->insertBlock();
+
+ BlockRestriction::update( [] );
+
+ $db = wfGetDb( DB_REPLICA );
+ $result = $db->select(
+ [ 'ipblocks_restrictions' ],
+ [ '*' ],
+ [ 'ir_ipb_id' => $block->getId() ]
+ );
+
+ $this->assertEquals( 0, $result->numRows() );
+ }
+
+ /**
+ * @covers ::update
+ * @covers ::restrictionsByBlockId
+ * @covers ::restrictionsToRemove
+ */
+ public function testUpdateSame() {
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ BlockRestriction::update( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ $db = wfGetDb( DB_REPLICA );
+ $result = $db->select(
+ [ 'ipblocks_restrictions' ],
+ [ '*' ],
+ [ 'ir_ipb_id' => $block->getId() ]
+ );
+
+ $this->assertEquals( 1, $result->numRows() );
+ $row = $result->fetchObject();
+ $this->assertEquals( $block->getId(), $row->ir_ipb_id );
+ $this->assertEquals( $page->getId(), $row->ir_value );
+ }
+
+ /**
+ * @covers ::updateByParentBlockId
+ */
+ public function testDeleteAllUpdateByParentBlockId() {
+ // Create a block and an autoblock (a child block)
+ $block = $this->insertBlock();
+ $pageFoo = $this->getExistingTestPage( 'Foo' );
+ $pageBar = $this->getExistingTestPage( 'Bar' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $pageFoo->getId() ),
+ ] );
+ $autoblockId = $block->doAutoblock( '127.0.0.1' );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+ $this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );
+
+ // Ensure that the restrictions on the autoblock are the same as the block.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 1, $restrictions );
+ $this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );
+
+ // Update the restrictions on the autoblock (but leave the block unchanged)
+ BlockRestriction::updateByParentBlockId( $block->getId(), [
+ new PageRestriction( $block->getId(), $pageBar->getId() ),
+ ] );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+ $this->assertEquals( $pageFoo->getId(), $restrictions[0]->getValue() );
+
+ // Ensure that the restrictions on the autoblock have been updated.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 1, $restrictions );
+ $this->assertEquals( $pageBar->getId(), $restrictions[0]->getValue() );
+ }
+
+ /**
+ * @covers ::updateByParentBlockId
+ */
+ public function testUpdateByParentBlockId() {
+ // Create a block and an autoblock (a child block)
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+ $autoblockId = $block->doAutoblock( '127.0.0.1' );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ // Ensure that the restrictions on the autoblock have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 1, $restrictions );
+
+ // Remove the restrictions on the autoblock (but leave the block unchanged)
+ BlockRestriction::updateByParentBlockId( $block->getId(), [] );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ // Ensure that the restrictions on the autoblock have been updated.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 0, $restrictions );
+ }
+
+ /**
+ * @covers ::updateByParentBlockId
+ */
+ public function testNoAutoblocksUpdateByParentBlockId() {
+ // Create a block with no autoblock.
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ // Update the restrictions on any autoblocks (there are none).
+ BlockRestriction::updateByParentBlockId( $block->getId(), $restrictions );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+ }
+
+ /**
+ * @covers ::delete
+ */
+ public function testDelete() {
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ $result = BlockRestriction::delete( array_merge( $restrictions, [ new \stdClass() ] ) );
+ $this->assertTrue( $result );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 0, $restrictions );
+ }
+
+ /**
+ * @covers ::deleteByBlockId
+ */
+ public function testDeleteByBlockId() {
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ $result = BlockRestriction::deleteByBlockId( $block->getId() );
+ $this->assertNotFalse( $result );
+
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 0, $restrictions );
+ }
+
+ /**
+ * @covers ::deleteByParentBlockId
+ */
+ public function testDeleteByParentBlockId() {
+ // Create a block with no autoblock.
+ $block = $this->insertBlock();
+ $page = $this->getExistingTestPage( 'Foo' );
+ BlockRestriction::insert( [
+ new PageRestriction( $block->getId(), $page->getId() ),
+ ] );
+ $autoblockId = $block->doAutoblock( '127.0.0.1' );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ // Ensure that the restrictions on the autoblock are the same as the block.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 1, $restrictions );
+
+ // Remove all of the restrictions on the autoblock (but leave the block unchanged).
+ $result = BlockRestriction::deleteByParentBlockId( $block->getId() );
+ // NOTE: commented out until https://gerrit.wikimedia.org/r/c/mediawiki/core/+/469324 is merged
+ //$this->assertTrue( $result );
+
+ // Ensure that the restrictions on the block have not changed.
+ $restrictions = BlockRestriction::loadByBlockId( $block->getId() );
+ $this->assertCount( 1, $restrictions );
+
+ // Ensure that the restrictions on the autoblock have been removed.
+ $restrictions = BlockRestriction::loadByBlockId( $autoblockId );
+ $this->assertCount( 0, $restrictions );
+ }
+
+ /**
+ * @covers ::equals
+ * @dataProvider equalsDataProvider
+ *
+ * @param array $a
+ * @param array $b
+ * @param bool $expected
+ */
+ public function testEquals( array $a, array $b, $expected ) {
+ $this->assertSame( $expected, BlockRestriction::equals( $a, $b ) );
+ }
+
+ public function equalsDataProvider() {
+ return [
+ [
+ [
+ new \stdClass(),
+ new PageRestriction( 1, 1 ),
+ ],
+ [
+ new \stdClass(),
+ new PageRestriction( 1, 2 )
+ ],
+ false,
+ ],
+ [
+ [
+ new PageRestriction( 1, 1 ),
+ ],
+ [
+ new PageRestriction( 1, 1 ),
+ new PageRestriction( 1, 2 )
+ ],
+ false,
+ ],
+ [
+ [],
+ [],
+ true,
+ ],
+ [
+ [
+ new PageRestriction( 1, 1 ),
+ new PageRestriction( 1, 2 ),
+ new PageRestriction( 2, 3 ),
+ ],
+ [
+ new PageRestriction( 2, 3 ),
+ new PageRestriction( 1, 2 ),
+ new PageRestriction( 1, 1 ),
+ ],
+ true
+ ],
+ ];
+ }
+
+ /**
+ * @covers ::setBlockId
+ */
+ public function testSetBlockId() {
+ $restrictions = [
+ new \stdClass(),
+ new PageRestriction( 1, 1 ),
+ new PageRestriction( 1, 2 ),
+ ];
+
+ $result = BlockRestriction::setBlockId( 2, $restrictions );
+
+ $this->assertSame( 1, $restrictions[1]->getBlockId() );
+ $this->assertSame( 1, $restrictions[2]->getBlockId() );
+ $this->assertSame( 2, $result[0]->getBlockId() );
+ $this->assertSame( 2, $result[1]->getBlockId() );
+ }
+
+ protected function insertBlock() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new \Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ 'sitewide' => 0,
+ 'enableAutoblock' => true,
+ ] );
+
+ $block->insert();
+
+ return $block;
+ }
+
+ protected function insertRestriction( $blockId, $type, $value ) {
+ $this->db->insert( 'ipblocks_restrictions', [
+ 'ir_ipb_id' => $blockId,
+ 'ir_type' => $type,
+ 'ir_value' => $value,
+ ] );
+ }
+
+ protected function resetTables() {
+ $this->db->delete( 'ipblocks', '*', __METHOD__ );
+ $this->db->delete( 'ipblocks_restrictions', '*', __METHOD__ );
+ }
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Block\Restriction;
+
+use MediaWiki\Block\Restriction\PageRestriction;
+
+/**
+ * @group Database
+ * @group Blocking
+ * @covers \MediaWiki\Block\Restriction\AbstractRestriction
+ * @covers \MediaWiki\Block\Restriction\PageRestriction
+ */
+class PageRestrictionTest extends RestrictionTestCase {
+
+ public function testMatches() {
+ $class = $this->getClass();
+ $page = $this->getExistingTestPage( 'Saturn' );
+ $restriction = new $class( 1, $page->getId() );
+ $this->assertTrue( $restriction->matches( $page->getTitle() ) );
+
+ $page = $this->getExistingTestPage( 'Mars' );
+ $this->assertFalse( $restriction->matches( $page->getTitle() ) );
+ }
+
+ public function testGetType() {
+ $class = $this->getClass();
+ $restriction = new $class( 1, 2 );
+ $this->assertEquals( 'page', $restriction->getType() );
+ }
+
+ public function testGetTitle() {
+ $class = $this->getClass();
+ $restriction = new $class( 1, 2 );
+ $title = \Title::newFromText( 'Pluto' );
+ $title->mArticleID = 2;
+ $restriction->setTitle( $title );
+ $this->assertSame( $title, $restriction->getTitle() );
+
+ $restriction = new $class( 1, 1 );
+ $title = \Title::newFromId( 1 );
+ $this->assertEquals( $title->getArticleId(), $restriction->getTitle()->getArticleId() );
+ }
+
+ public function testNewFromRow() {
+ $class = $this->getClass();
+ $restriction = $class::newFromRow( (object)[
+ 'ir_ipb_id' => 1,
+ 'ir_value' => 2,
+ 'page_namespace' => 0,
+ 'page_title' => 'Saturn',
+ ] );
+
+ $this->assertSame( 1, $restriction->getBlockId() );
+ $this->assertSame( 2, $restriction->getValue() );
+ $this->assertSame( 'Saturn', $restriction->getTitle()->getText() );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getClass() {
+ return PageRestriction::class;
+ }
+}
--- /dev/null
+<?php
+
+namespace MediaWiki\Tests\Block\Restriction;
+
+/**
+ * @group Blocking
+ */
+abstract class RestrictionTestCase extends \MediaWikiTestCase {
+ public function testConstruct() {
+ $class = $this->getClass();
+ $restriction = new $class( 1, 2 );
+
+ $this->assertSame( $restriction->getBlockId(), 1 );
+ $this->assertSame( $restriction->getValue(), 2 );
+ }
+
+ public function testSetBlockId() {
+ $class = $this->getClass();
+ $restriction = new $class( 1, 2 );
+
+ $restriction->setBlockId( 10 );
+ $this->assertSame( $restriction->getBlockId(), 10 );
+ }
+
+ public function testEquals() {
+ $class = $this->getClass();
+
+ // Test two restrictions with the same data.
+ $restriction = new $class( 1, 2 );
+ $second = new $class( 1, 2 );
+ $this->assertTrue( $restriction->equals( $second ) );
+
+ // Test two restrictions that implement different classes.
+ $second = $this->createMock( $this->getClass() );
+ $this->assertFalse( $restriction->equals( $second ) );
+
+ // Not the same block id.
+ $second = new $class( 2, 2 );
+ $this->assertTrue( $restriction->equals( $second ) );
+
+ // Not the same value.
+ $second = new $class( 1, 3 );
+ $this->assertFalse( $restriction->equals( $second ) );
+ }
+
+ public function testNewFromRow() {
+ $class = $this->getClass();
+
+ $restriction = $class::newFromRow( (object)[
+ 'ir_ipb_id' => 1,
+ 'ir_value' => 2,
+ ] );
+
+ $this->assertSame( 1, $restriction->getBlockId() );
+ $this->assertSame( 2, $restriction->getValue() );
+ }
+
+ public function testToRow() {
+ $class = $this->getClass();
+
+ $restriction = new $class( 1, 2 );
+ $row = $restriction->toRow();
+
+ $this->assertSame( 1, $row['ir_ipb_id'] );
+ $this->assertSame( 2, $row['ir_value'] );
+ }
+
+ /**
+ * Get the class name of the class that is being tested.
+ *
+ * @return string
+ */
+ abstract protected function getClass();
+}
public function testSuppressReblockLogDatabaseRows( $row, $extra ) {
$this->doTestLogFormatter( $row, $extra );
}
+
+ public function providePartialBlockLogDatabaseRows() {
+ return [
+ [
+ [
+ 'type' => 'block',
+ 'action' => 'block',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ '5::duration' => 'infinite',
+ '6::flags' => 'anononly',
+ '7::restrictions' => [ 'pages' => [ 'User:Test1', 'Main Page' ] ],
+ 'sitewide' => false,
+ ],
+ ],
+ [
+ 'text' => 'Sysop blocked Logtestuser from editing the pages User:Test1 and Main Page'
+ . ' with an expiration time of indefinite (anonymous users only)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ 'restrictions' => [ 'pages' => [
+ [
+ 'page_ns' => 2,
+ 'page_title' => 'User:Test1',
+ ], [
+ 'page_ns' => 0,
+ 'page_title' => 'Main Page',
+ ],
+ ],
+ ],
+ 'sitewide' => false,
+ ],
+ ],
+ ],
+ [
+ [
+ 'type' => 'block',
+ 'action' => 'block',
+ 'comment' => 'Block comment',
+ 'user' => 0,
+ 'user_text' => 'Sysop',
+ 'namespace' => NS_USER,
+ 'title' => 'Logtestuser',
+ 'params' => [
+ '5::duration' => 'infinite',
+ '6::flags' => 'anononly',
+ 'sitewide' => false,
+ ],
+ ],
+ [
+ 'text' => 'Sysop blocked Logtestuser from non-editing actions'
+ . ' with an expiration time of indefinite (anonymous users only)',
+ 'api' => [
+ 'duration' => 'infinite',
+ 'flags' => [ 'anononly' ],
+ 'sitewide' => false,
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providePartialBlockLogDatabaseRows
+ */
+ public function testPartialBlockLogDatabaseRows( $row, $extra ) {
+ $this->doTestLogFormatter( $row, $extra );
+ }
}
"#REDIRECT [[Media:hello_world]]",
"File:Hello world"
],
+ // Test fragments longer than 255 bytes (T207876)
+ [
+ 'WikiPageTest_testGetRedirectTarget_4',
+ CONTENT_MODEL_WIKITEXT,
+ // phpcs:ignore Generic.Files.LineLength
+ '#REDIRECT [[Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴]]',
+ // phpcs:ignore Generic.Files.LineLength
+ 'Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴...'
+ ]
];
}
# now, test the actual redirect
$t = $page->getRedirectTarget();
- $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
+ $this->assertEquals( $target, is_null( $t ) ? null : $t->getFullText() );
}
/**
--- /dev/null
+<?php
+
+use MediaWiki\Block\BlockRestriction;
+use MediaWiki\Block\Restriction\PageRestriction;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Blocking
+ * @group Database
+ * @coversDefaultClass SpecialBlock
+ */
+class SpecialBlockTest extends SpecialPageTestBase {
+ /**
+ * {@inheritdoc}
+ */
+ protected function newSpecialPage() {
+ return new SpecialBlock();
+ }
+
+ public function tearDown() {
+ parent::tearDown();
+ $this->resetTables();
+ }
+
+ /**
+ * @covers ::getFormFields()
+ */
+ public function testGetFormFields() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => false,
+ ] );
+ $page = $this->newSpecialPage();
+ $wrappedPage = TestingAccessWrapper::newFromObject( $page );
+ $fields = $wrappedPage->getFormFields();
+ $this->assertInternalType( 'array', $fields );
+ $this->assertArrayHasKey( 'Target', $fields );
+ $this->assertArrayHasKey( 'Expiry', $fields );
+ $this->assertArrayHasKey( 'Reason', $fields );
+ $this->assertArrayHasKey( 'CreateAccount', $fields );
+ $this->assertArrayHasKey( 'DisableUTEdit', $fields );
+ $this->assertArrayHasKey( 'DisableUTEdit', $fields );
+ $this->assertArrayHasKey( 'AutoBlock', $fields );
+ $this->assertArrayHasKey( 'HardBlock', $fields );
+ $this->assertArrayHasKey( 'PreviousTarget', $fields );
+ $this->assertArrayHasKey( 'Confirm', $fields );
+
+ $this->assertArrayNotHasKey( 'EditingRestriction', $fields );
+ $this->assertArrayNotHasKey( 'PageRestrictions', $fields );
+ }
+
+ /**
+ * @covers ::getFormFields()
+ */
+ public function testGetFormFieldsPartialBlocks() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+ $page = $this->newSpecialPage();
+ $wrappedPage = TestingAccessWrapper::newFromObject( $page );
+ $fields = $wrappedPage->getFormFields();
+
+ $this->assertArrayHasKey( 'EditingRestriction', $fields );
+ $this->assertArrayHasKey( 'PageRestrictions', $fields );
+ }
+
+ /**
+ * @covers ::maybeAlterFormDefaults()
+ */
+ public function testMaybeAlterFormDefaults() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => false,
+ ] );
+
+ $block = $this->insertBlock();
+
+ // Refresh the block from the database.
+ $block = Block::newFromTarget( $block->getTarget() );
+
+ $page = $this->newSpecialPage();
+
+ $wrappedPage = TestingAccessWrapper::newFromObject( $page );
+ $wrappedPage->target = $block->getTarget();
+ $fields = $wrappedPage->getFormFields();
+
+ $this->assertSame( (string)$block->getTarget(), $fields['Target']['default'] );
+ $this->assertSame( $block->isHardblock(), $fields['HardBlock']['default'] );
+ $this->assertSame( $block->prevents( 'createaccount' ), $fields['CreateAccount']['default'] );
+ $this->assertSame( $block->isAutoblocking(), $fields['AutoBlock']['default'] );
+ $this->assertSame( $block->prevents( 'editownusertalk' ), $fields['DisableUTEdit']['default'] );
+ $this->assertSame( $block->mReason, $fields['Reason']['default'] );
+ $this->assertSame( 'infinite', $fields['Expiry']['default'] );
+ }
+
+ /**
+ * @covers ::maybeAlterFormDefaults()
+ */
+ public function testMaybeAlterFormDefaultsPartial() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+ $pageSaturn = $this->getExistingTestPage( 'Saturn' );
+ $pageMars = $this->getExistingTestPage( 'Mars' );
+
+ $block = new \Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ 'sitewide' => 0,
+ 'enableAutoblock' => true,
+ ] );
+
+ $block->setRestrictions( [
+ new PageRestriction( 0, $pageSaturn->getId() ),
+ new PageRestriction( 0, $pageMars->getId() ),
+ ] );
+
+ $block->insert();
+
+ // Refresh the block from the database.
+ $block = Block::newFromTarget( $block->getTarget() );
+
+ $page = $this->newSpecialPage();
+
+ $wrappedPage = TestingAccessWrapper::newFromObject( $page );
+ $wrappedPage->target = $block->getTarget();
+ $fields = $wrappedPage->getFormFields();
+
+ $titles = [
+ $pageMars->getTitle()->getPrefixedText(),
+ $pageSaturn->getTitle()->getPrefixedText(),
+ ];
+
+ $this->assertSame( (string)$block->getTarget(), $fields['Target']['default'] );
+ $this->assertSame( 'partial', $fields['EditingRestriction']['default'] );
+ $this->assertSame( implode( "\n", $titles ), $fields['PageRestrictions']['default'] );
+ }
+
+ /**
+ * @covers ::processForm()
+ */
+ public function testProcessForm() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => false,
+ ] );
+ $badActor = $this->getTestUser()->getUser();
+ $context = RequestContext::getMain();
+
+ $page = $this->newSpecialPage();
+ $reason = 'test';
+ $expiry = 'infinity';
+ $data = [
+ 'Target' => (string)$badActor,
+ 'Expiry' => 'infinity',
+ 'Reason' => [
+ $reason,
+ ],
+ 'Confirm' => '1',
+ 'CreateAccount' => '0',
+ 'DisableUTEdit' => '0',
+ 'DisableEmail' => '0',
+ 'HardBlock' => '0',
+ 'AutoBlock' => '1',
+ 'HideUser' => '0',
+ 'Watch' => '0',
+ ];
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ }
+
+ /**
+ * @covers ::processForm()
+ */
+ public function testProcessFormExisting() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => false,
+ ] );
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+ $context = RequestContext::getMain();
+
+ // Create a block that will be updated.
+ $block = new \Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ 'sitewide' => 0,
+ 'enableAutoblock' => false,
+ ] );
+ $block->insert();
+
+ $page = $this->newSpecialPage();
+ $reason = 'test';
+ $expiry = 'infinity';
+ $data = [
+ 'Target' => (string)$badActor,
+ 'Expiry' => 'infinity',
+ 'Reason' => [
+ $reason,
+ ],
+ 'Confirm' => '1',
+ 'CreateAccount' => '0',
+ 'DisableUTEdit' => '0',
+ 'DisableEmail' => '0',
+ 'HardBlock' => '0',
+ 'AutoBlock' => '1',
+ 'HideUser' => '0',
+ 'Watch' => '0',
+ ];
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertSame( '1', $block->isAutoblocking() );
+ }
+
+ /**
+ * @covers ::processForm()
+ */
+ public function testProcessFormRestictions() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+ $badActor = $this->getTestUser()->getUser();
+ $context = RequestContext::getMain();
+
+ $pageSaturn = $this->getExistingTestPage( 'Saturn' );
+ $pageMars = $this->getExistingTestPage( 'Mars' );
+
+ $titles = [
+ $pageSaturn->getTitle()->getText(),
+ $pageMars->getTitle()->getText(),
+ ];
+
+ $page = $this->newSpecialPage();
+ $reason = 'test';
+ $expiry = 'infinity';
+ $data = [
+ 'Target' => (string)$badActor,
+ 'Expiry' => 'infinity',
+ 'Reason' => [
+ $reason,
+ ],
+ 'Confirm' => '1',
+ 'CreateAccount' => '0',
+ 'DisableUTEdit' => '0',
+ 'DisableEmail' => '0',
+ 'HardBlock' => '0',
+ 'AutoBlock' => '1',
+ 'HideUser' => '0',
+ 'Watch' => '0',
+ 'EditingRestriction' => 'partial',
+ 'PageRestrictions' => implode( "\n", $titles ),
+ ];
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertCount( 2, $block->getRestrictions() );
+ $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [
+ new PageRestriction( $block->getId(), $pageMars->getId() ),
+ new PageRestriction( $block->getId(), $pageSaturn->getId() ),
+ ] ) );
+ }
+
+ /**
+ * @covers ::processForm()
+ */
+ public function testProcessFormRestrictionsChange() {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => true,
+ ] );
+ $badActor = $this->getTestUser()->getUser();
+ $context = RequestContext::getMain();
+
+ $pageSaturn = $this->getExistingTestPage( 'Saturn' );
+ $pageMars = $this->getExistingTestPage( 'Mars' );
+
+ $titles = [
+ $pageSaturn->getTitle()->getText(),
+ $pageMars->getTitle()->getText(),
+ ];
+
+ // Create a partial block.
+ $page = $this->newSpecialPage();
+ $reason = 'test';
+ $expiry = 'infinity';
+ $data = [
+ 'Target' => (string)$badActor,
+ 'Expiry' => 'infinity',
+ 'Reason' => [
+ $reason,
+ ],
+ 'Confirm' => '1',
+ 'CreateAccount' => '0',
+ 'DisableUTEdit' => '0',
+ 'DisableEmail' => '0',
+ 'HardBlock' => '0',
+ 'AutoBlock' => '1',
+ 'HideUser' => '0',
+ 'Watch' => '0',
+ 'EditingRestriction' => 'partial',
+ 'PageRestrictions' => implode( "\n", $titles ),
+ ];
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertFalse( $block->isSitewide() );
+ $this->assertCount( 2, $block->getRestrictions() );
+ $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [
+ new PageRestriction( $block->getId(), $pageMars->getId() ),
+ new PageRestriction( $block->getId(), $pageSaturn->getId() ),
+ ] ) );
+
+ // Remove a page from the partial block.
+ $data['PageRestrictions'] = $pageMars->getTitle()->getText();
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertFalse( $block->isSitewide() );
+ $this->assertCount( 1, $block->getRestrictions() );
+ $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [
+ new PageRestriction( $block->getId(), $pageMars->getId() ),
+ ] ) );
+
+ // Remove the last page from the block.
+ $data['PageRestrictions'] = '';
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertFalse( $block->isSitewide() );
+ $this->assertCount( 0, $block->getRestrictions() );
+
+ // Change to sitewide.
+ $data['EditingRestriction'] = 'sitewide';
+ $result = $page->processForm( $data, $context );
+
+ $this->assertTrue( $result );
+
+ $block = Block::newFromTarget( $badActor );
+ $this->assertSame( $reason, $block->mReason );
+ $this->assertSame( $expiry, $block->getExpiry() );
+ $this->assertTrue( $block->isSitewide() );
+ $this->assertCount( 0, $block->getRestrictions() );
+
+ // Ensure that there are no restrictions where the blockId is 0.
+ $count = $this->db->selectRowCount(
+ 'ipblocks_restrictions',
+ '*',
+ [ 'ir_ipb_id' => 0 ],
+ __METHOD__
+ );
+ $this->assertSame( 0, $count );
+ }
+
+ protected function insertBlock() {
+ $badActor = $this->getTestUser()->getUser();
+ $sysop = $this->getTestSysop()->getUser();
+
+ $block = new \Block( [
+ 'address' => $badActor->getName(),
+ 'user' => $badActor->getId(),
+ 'by' => $sysop->getId(),
+ 'expiry' => 'infinity',
+ 'sitewide' => 1,
+ 'enableAutoblock' => true,
+ ] );
+
+ $block->insert();
+
+ return $block;
+ }
+
+ protected function resetTables() {
+ $this->db->delete( 'ipblocks', '*', __METHOD__ );
+ $this->db->delete( 'ipblocks_restrictions', '*', __METHOD__ );
+ }
+}
--- /dev/null
+<?php
+
+use MediaWiki\Block\Restriction\PageRestriction;
+use MediaWiki\MediaWikiServices;
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @group Database
+ * @coversDefaultClass BlockListPager
+ */
+class BlockListPagerTest extends MediaWikiTestCase {
+
+ /**
+ * @covers ::formatValue
+ * @dataProvider formatValueEmptyProvider
+ * @dataProvider formatValueDefaultProvider
+ * @param string $name
+ * @param string $value
+ * @param string $expected
+ */
+ public function testFormatValue( $name, $value, $expected, $row = null ) {
+ $this->setMwGlobals( [
+ 'wgEnablePartialBlocks' => false,
+ ] );
+ $row = $row ?: new stdClass;
+ $pager = new BlockListPager( new SpecialPage(), [] );
+ $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
+ $wrappedPager->mCurrentRow = $row;
+
+ $formatted = $pager->formatValue( $name, $value );
+ $this->assertEquals( $expected, $formatted );
+ }
+
+ /**
+ * Test empty values.
+ */
+ public function formatValueEmptyProvider() {
+ return [
+ [
+ 'test',
+ '',
+ 'Unable to format test',
+ ],
+ [
+ 'ipb_timestamp',
+ wfTimestamp( TS_UNIX ),
+ date( 'H:i, j F Y' ),
+ ],
+ [
+ 'ipb_expiry',
+ '',
+ 'infinite<br />0 minutes left',
+ ],
+ ];
+ }
+
+ /**
+ * Test the default row values.
+ */
+ public function formatValueDefaultProvider() {
+ $row = (object)[
+ 'ipb_user' => 0,
+ 'ipb_address' => '127.0.0.1',
+ 'ipb_by_text' => 'Admin',
+ 'ipb_create_account' => 1,
+ 'ipb_auto' => 0,
+ 'ipb_anon_only' => 0,
+ 'ipb_create_account' => 1,
+ 'ipb_enable_autoblock' => 1,
+ 'ipb_deleted' => 0,
+ 'ipb_block_email' => 0,
+ 'ipb_allow_usertalk' => 0,
+ 'ipb_sitewide' => 1,
+ ];
+
+ return [
+ [
+ 'test',
+ '',
+ 'Unable to format test',
+ $row,
+ ],
+ [
+ 'ipb_timestamp',
+ wfTimestamp( TS_UNIX ),
+ date( 'H:i, j F Y' ),
+ $row,
+ ],
+ [
+ 'ipb_expiry',
+ '',
+ 'infinite<br />0 minutes left',
+ $row,
+ ],
+ [
+ 'ipb_by',
+ '',
+ $row->ipb_by_text,
+ $row,
+ ],
+ [
+ 'ipb_params',
+ '',
+ '<ul><li>account creation disabled</li><li>cannot edit own talk page</li></ul>',
+ $row,
+ ]
+ ];
+ }
+
+ /**
+ * @covers ::formatValue
+ */
+ public function testFormatValueRestrictions() {
+ $pager = new BlockListPager( new SpecialPage(), [] );
+
+ $row = (object)[
+ 'ipb_id' => 0,
+ 'ipb_user' => 0,
+ 'ipb_anon_only' => 0,
+ 'ipb_enable_autoblock' => 0,
+ 'ipb_create_account' => 0,
+ 'ipb_block_email' => 0,
+ 'ipb_allow_usertalk' => 1,
+ 'ipb_sitewide' => 0,
+ ];
+ $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
+ $wrappedPager->mCurrentRow = $row;
+
+ $pageName = 'Victor Frankenstein';
+ $page = $this->insertPage( $pageName );
+ $title = $page['title'];
+ $pageId = $page['id'];
+
+ $restrictions = [
+ ( new PageRestriction( 0, $pageId ) )->setTitle( $title )
+ ];
+
+ $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
+ $wrappedPager->restrictions = $restrictions;
+
+ $formatted = $pager->formatValue( 'ipb_params', '' );
+ $this->assertEquals( '<ul><li>'
+ . wfMessage( 'blocklist-editing' )->text()
+ . '<ul><li><a href="/index.php/'
+ . $title->getDBKey()
+ . '" title="'
+ . $pageName
+ . '">'
+ . $pageName
+ . '</a></li></ul></li></ul>',
+ $formatted
+ );
+ }
+
+ /**
+ * @covers ::preprocessResults
+ */
+ public function testPreprocessResults() {
+ // Test the Link Cache.
+ $linkCache = MediaWikiServices::getInstance()->getLinkCache();
+ $wrappedlinkCache = TestingAccessWrapper::newFromObject( $linkCache );
+
+ $links = [
+ 'User:127.0.0.1',
+ 'User_talk:127.0.0.1',
+ 'User:Admin',
+ 'User_talk:Admin',
+ ];
+
+ foreach ( $links as $link ) {
+ $this->assertNull( $wrappedlinkCache->badLinks->get( $link ) );
+ }
+
+ $row = (object)[
+ 'ipb_address' => '127.0.0.1',
+ 'by_user_name' => 'Admin',
+ 'ipb_sitewide' => 1,
+ 'ipb_timestamp' => $this->db->timestamp( wfTimestamp( TS_MW ) ),
+ ];
+ $pager = new BlockListPager( new SpecialPage(), [] );
+ $pager->preprocessResults( [ $row ] );
+
+ foreach ( $links as $link ) {
+ $this->assertSame( 1, $wrappedlinkCache->badLinks->get( $link ) );
+ }
+
+ // Test Sitewide Blocks.
+ $row = (object)[
+ 'ipb_address' => '127.0.0.1',
+ 'by_user_name' => 'Admin',
+ 'ipb_sitewide' => 1,
+ ];
+ $pager = new BlockListPager( new SpecialPage(), [] );
+ $pager->preprocessResults( [ $row ] );
+
+ $this->assertObjectNotHasAttribute( 'ipb_restrictions', $row );
+
+ $pageName = 'Victor Frankenstein';
+ $page = $this->getExistingTestPage( 'Victor Frankenstein' );
+ $title = $page->getTitle();
+
+ $target = '127.0.0.1';
+
+ // Test Partial Blocks Blocks.
+ $block = new Block( [
+ 'address' => $target,
+ 'by' => $this->getTestSysop()->getUser()->getId(),
+ 'reason' => 'Parce que',
+ 'expiry' => $this->db->getInfinity(),
+ 'sitewide' => false,
+ ] );
+ $block->setRestrictions( [
+ new PageRestriction( 0, $page->getId() ),
+ ] );
+ $block->insert();
+
+ $result = $this->db->select( 'ipblocks', [ '*' ], [ 'ipb_id' => $block->getId() ] );
+
+ $pager = new BlockListPager( new SpecialPage(), [] );
+ $pager->preprocessResults( $result );
+
+ $wrappedPager = TestingAccessWrapper::newFromObject( $pager );
+
+ $restrictions = $wrappedPager->restrictions;
+ $this->assertInternalType( 'array', $restrictions );
+
+ $restriction = $restrictions[0];
+ $this->assertEquals( $page->getId(), $restriction->getValue() );
+ $this->assertEquals( $page->getId(), $restriction->getTitle()->getArticleId() );
+ $this->assertEquals( $title->getDBKey(), $restriction->getTitle()->getDBKey() );
+ $this->assertEquals( $title->getNamespace(), $restriction->getTitle()->getNamespace() );
+
+ // Delete the block and the restrictions.
+ $block->delete();
+ }
+}