==== Upgraded external libraries ====
* Updated jquery.chosen from v0.9.14 to v1.8.2.
+* Updated composer/spdx-licenses from 1.1.4 to
+ 1.2.0 (development dependency).
* …
==== New external libraries ====
"zordius/lightncandy": "0.23"
},
"require-dev": {
- "composer/spdx-licenses": "1.1.4",
+ "composer/spdx-licenses": "1.2.0",
"hamcrest/hamcrest-php": "^2.0",
"jakub-onderka/php-parallel-lint": "0.9.2",
"jetbrains/phpstorm-stubs": "dev-master#1b9906084d6635456fcf3f3a01f0d7d5b99a578a",
* Find a file.
* Shortcut for RepoGroup::singleton()->findFile()
*
- * @param string $title String or Title object
+ * @param string|Title $title String or Title object
* @param array $options Associative array of options (see RepoGroup::findFile)
* @return File|bool File, or false if the file does not exist
*/
if ( function_exists( 'register_postsend_function' ) ) {
// https://github.com/facebook/hhvm/issues/1230
register_postsend_function( $callback );
+ /** @noinspection PhpUnusedLocalVariableInspection */
$blocksHttpClient = false;
} else {
if ( function_exists( 'fastcgi_finish_request' ) ) {
fastcgi_finish_request();
+ /** @noinspection PhpUnusedLocalVariableInspection */
$blocksHttpClient = false;
} else {
// Either all DB and deferred updates should happen or none.
$storeWiki = $storeWiki ?: wfWikiID();
$dbWiki = $dbWiki ?: wfWikiID();
- if ( $dbWiki !== $storeWiki ) {
- throw new MWException( "RevisionStore for $storeWiki "
- . "cannot be used with a DB connection for $dbWiki" );
+ if ( $dbWiki === $storeWiki ) {
+ return;
+ }
+
+ // HACK: counteract encoding imposed by DatabaseDomain
+ $storeWiki = str_replace( '?h', '-', $storeWiki );
+ $dbWiki = str_replace( '?h', '-', $dbWiki );
+
+ if ( $dbWiki === $storeWiki ) {
+ return;
}
+
+ throw new MWException( "RevisionStore for $storeWiki "
+ . "cannot be used with a DB connection for $dbWiki" );
}
/**
$this->dieWithError( 'apierror-baddiff' );
}
+ // Extract sections, if told to
+ if ( isset( $params['fromsection'] ) ) {
+ $fromContent = $fromContent->getSection( $params['fromsection'] );
+ if ( !$fromContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchfromsection', wfEscapeWikiText( $params['fromsection'] ) ],
+ 'nosuchfromsection'
+ );
+ }
+ }
+ if ( isset( $params['tosection'] ) ) {
+ $toContent = $toContent->getSection( $params['tosection'] );
+ if ( !$toContent ) {
+ $this->dieWithError(
+ [ 'apierror-compare-nosuchtosection', wfEscapeWikiText( $params['tosection'] ) ],
+ 'nosuchtosection'
+ );
+ }
+ }
+
// Get the diff
$context = new DerivativeContext( $this->getContext() );
if ( $relRev && $relRev->getTitle() ) {
'text' => [
ApiBase::PARAM_TYPE => 'text'
],
+ 'section' => null,
'pst' => false,
'contentformat' => [
ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
"apihelp-compare-param-fromid": "First page ID to compare.",
"apihelp-compare-param-fromrev": "First revision to compare.",
"apihelp-compare-param-fromtext": "Use this text instead of the content of the revision specified by <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>.",
+ "apihelp-compare-param-fromsection": "Only use the specified section of the specified 'from' content.",
"apihelp-compare-param-frompst": "Do a pre-save transform on <var>fromtext</var>.",
"apihelp-compare-param-fromcontentmodel": "Content model of <var>fromtext</var>. If not supplied, it will be guessed based on the other parameters.",
"apihelp-compare-param-fromcontentformat": "Content serialization format of <var>fromtext</var>.",
"apihelp-compare-param-torev": "Second revision to compare.",
"apihelp-compare-param-torelative": "Use a revision relative to the revision determined from <var>fromtitle</var>, <var>fromid</var> or <var>fromrev</var>. All of the other 'to' options will be ignored.",
"apihelp-compare-param-totext": "Use this text instead of the content of the revision specified by <var>totitle</var>, <var>toid</var> or <var>torev</var>.",
+ "apihelp-compare-param-tosection": "Only use the specified section of the specified 'to' content.",
"apihelp-compare-param-topst": "Do a pre-save transform on <var>totext</var>.",
"apihelp-compare-param-tocontentmodel": "Content model of <var>totext</var>. If not supplied, it will be guessed based on the other parameters.",
"apihelp-compare-param-tocontentformat": "Content serialization format of <var>totext</var>.",
"apierror-chunk-too-small": "Minimum chunk size is $1 {{PLURAL:$1|byte|bytes}} for non-final chunks.",
"apierror-cidrtoobroad": "$1 CIDR ranges broader than /$2 are not accepted.",
"apierror-compare-no-title": "Cannot pre-save transform without a title. Try specifying <var>fromtitle</var> or <var>totitle</var>.",
+ "apierror-compare-nosuchfromsection": "There is no section $1 in the 'from' content.",
+ "apierror-compare-nosuchtosection": "There is no section $1 in the 'to' content.",
"apierror-compare-relative-to-nothing": "No 'from' revision for <var>torelative</var> to be relative to.",
"apierror-contentserializationexception": "Content serialization failed: $1",
"apierror-contenttoobig": "The content you supplied exceeds the article size limit of $1 {{PLURAL:$1|kilobyte|kilobytes}}.",
"apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
"apihelp-compare-param-fromrev": "{{doc-apihelp-param|compare|fromrev}}",
"apihelp-compare-param-fromtext": "{{doc-apihelp-param|compare|fromtext}}",
+ "apihelp-compare-param-fromsection": "{{doc-apihelp-param|compare|fromsection}}",
"apihelp-compare-param-frompst": "{{doc-apihelp-param|compare|frompst}}",
"apihelp-compare-param-fromcontentmodel": "{{doc-apihelp-param|compare|fromcontentmodel}}",
"apihelp-compare-param-fromcontentformat": "{{doc-apihelp-param|compare|fromcontentformat}}",
"apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
"apihelp-compare-param-torelative": "{{doc-apihelp-param|compare|torelative}}",
"apihelp-compare-param-totext": "{{doc-apihelp-param|compare|totext}}",
+ "apihelp-compare-param-tosection": "{{doc-apihelp-param|compare|tosection}}",
"apihelp-compare-param-topst": "{{doc-apihelp-param|compare|topst}}",
"apihelp-compare-param-tocontentmodel": "{{doc-apihelp-param|compare|tocontentmodel}}",
"apihelp-compare-param-tocontentformat": "{{doc-apihelp-param|compare|tocontentformat}}",
"apierror-chunk-too-small": "{{doc-apierror}}\n\nParameters:\n* $1 - Minimum size in bytes.",
"apierror-cidrtoobroad": "{{doc-apierror}}\n\nParameters:\n* $1 - \"IPv4\" or \"IPv6\"\n* $2 - Minimum CIDR mask length.",
"apierror-compare-no-title": "{{doc-apierror}}",
+ "apierror-compare-nosuchfromsection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
+ "apierror-compare-nosuchtosection": "{{doc-apierror}}\n\nParameters:\n* $1 - Section identifier. Probably a number or \"T-\" followed by a number.",
"apierror-compare-relative-to-nothing": "{{doc-apierror}}",
"apierror-contentserializationexception": "{{doc-apierror}}\n\nParameters:\n* $1 - Exception text, may end with punctuation. Currently this is probably English, hopefully we'll fix that in the future.",
"apierror-contenttoobig": "{{doc-apierror}}\n\nParameters:\n* $1 - Maximum article size in kilobytes.",
$dbkey = $title->getDBkey();
if ( empty( $options['ignoreRedirect'] )
&& empty( $options['private'] )
- && empty( $options['bypassCache'] )
+ && empty( $options['latest'] )
) {
$time = isset( $options['time'] ) ? $options['time'] : '';
if ( $this->cache->has( $dbkey, $time, 60 ) ) {
const DEFAULT_ROWS = 25;
protected $mPlaceholder = '';
+ protected $mUseEditFont = false;
/**
* @param array $params
* - cols, rows: textarea size
* - placeholder/placeholder-message: set HTML placeholder attribute
* - spellcheck: set HTML spellcheck attribute
+ * - useeditfont: add CSS classes to use the same font as the wikitext editor
*/
public function __construct( $params ) {
parent::__construct( $params );
} elseif ( isset( $params['placeholder'] ) ) {
$this->mPlaceholder = $params['placeholder'];
}
+
+ if ( isset( $params['useeditfont'] ) ) {
+ $this->mUseEditFont = $params['useeditfont'];
+ }
}
public function getCols() {
}
public function getInputHTML( $value ) {
+ $classes = [];
+
$attribs = [
'id' => $this->mID,
'cols' => $this->getCols(),
] + $this->getTooltipAndAccessKey();
if ( $this->mClass !== '' ) {
- $attribs['class'] = $this->mClass;
+ array_push( $classes, $this->mClass );
+ }
+ if ( $this->mUseEditFont ) {
+ // The following classes can be used here:
+ // * mw-editfont-monospace
+ // * mw-editfont-sans-serif
+ // * mw-editfont-serif
+ array_push(
+ $classes,
+ 'mw-editfont-' . $this->mParent->getUser()->getOption( 'editfont' )
+ );
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.editfont.styles' );
}
if ( $this->mPlaceholder !== '' ) {
$attribs['placeholder'] = $this->mPlaceholder;
}
+ if ( count( $classes ) ) {
+ $attribs['class'] = implode( ' ', $classes );
+ }
$allowedParams = [
'tabindex',
}
function getInputOOUI( $value ) {
+ $classes = [];
+
if ( isset( $this->mParams['cols'] ) ) {
throw new Exception( "OOUIHTMLForm does not support the 'cols' parameter for textareas" );
}
$attribs = $this->getTooltipAndAccessKeyOOUI();
if ( $this->mClass !== '' ) {
- $attribs['classes'] = [ $this->mClass ];
+ array_push( $classes, $this->mClass );
+ }
+ if ( $this->mUseEditFont ) {
+ // The following classes can be used here:
+ // * mw-editfont-monospace
+ // * mw-editfont-sans-serif
+ // * mw-editfont-serif
+ array_push(
+ $classes,
+ 'mw-editfont-' . $this->mParent->getUser()->getOption( 'editfont' )
+ );
+ $this->mParent->getOutput()->addModuleStyles( 'mediawiki.editfont.styles' );
}
if ( $this->mPlaceholder !== '' ) {
$attribs['placeholder'] = $this->mPlaceholder;
}
+ if ( count( $classes ) ) {
+ $attribs['classes'] = $classes;
+ }
$allowedParams = [
'tabindex',
// 1.31
[ 'addTable', 'slots', 'patch-slots-table.sql' ],
[ 'addTable', 'content', 'patch-content-table.sql' ],
- [ 'addTable', 'content_moddels', 'patch-content_models-table.sql' ],
+ [ 'addTable', 'content_models', 'patch-content_models-table.sql' ],
[ 'addTable', 'slot_roles', 'patch-slot_roles-table.sql' ],
];
}
namespace Wikimedia\Rdbms;
-use Exception;
+use RuntimeException;
/**
* Database error base class
* @ingroup Database
*/
-class DBError extends Exception {
+class DBError extends RuntimeException {
/** @var IDatabase|null */
public $db;
'rows' => 10,
'default' => $input,
'id' => 'input',
- // The following classes can be used here:
- // * mw-editfont-monospace
- // * mw-editfont-sans-serif
- // * mw-editfont-serif
- 'cssclass' => 'mw-editfont-' . $this->getUser()->getOption( 'editfont' ),
+ 'useeditfont' => true,
],
'removecomments' => [
'type' => 'check',
],
];
- $this->getOutput()->addModuleStyles( 'mediawiki.editfont.styles' );
-
$form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
$form
->setSubmitTextMsg( 'expand_templates_ok' )
)
)
);
+ $config['namespace']['dropdown']['$overlay'] = true;
return parent::getConfig( $config );
}
}
public function getConfig( &$config ) {
$config['namespace'] = $this->config['namespace'];
+ $config['namespace']['dropdown']['$overlay'] = true;
$config['title'] = $this->config['title'];
+ $config['title']['$overlay'] = true;
return parent::getConfig( $config );
}
}
protected $precision = null;
protected $mustBeAfter = null;
protected $mustBeBefore = null;
- protected $overlay = null;
/**
* @param array $config Configuration options
* In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
* @param string $config['mustBeBefore'] Validates the date to be before this.
* In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`.
- * @param string $config['overlay'] The jQuery selector for the overlay layer on which to render
- * the calendar. This configuration is useful in cases where the expanded calendar is larger
- * than its container. The specified overlay layer is usually on top of the container and has
- * a larger area. Applicable only if the widget is infused. By default, the calendar uses
- * relative positioning.
*/
public function __construct( array $config = [] ) {
$config = array_merge( [
if ( isset( $config['placeholderLabel'] ) ) {
$this->placeholderLabel = $config['placeholderLabel'];
}
- if ( isset( $config['overlay'] ) ) {
- $this->overlay = $config['overlay'];
- }
// Set up placeholder text visible if the browser doesn't override it (logic taken from JS)
if ( $this->placeholderDateFormat !== null ) {
if ( $this->mustBeBefore !== null ) {
$config['mustBeBefore'] = $this->mustBeBefore;
}
- if ( $this->overlay !== null ) {
- $config['overlay'] = $this->overlay;
- }
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
$config['includeAllValue'] = $this->includeAllValue;
$config['exclude'] = $this->exclude;
// Skip DropdownInputWidget's getConfig(), we don't need 'options' config
+ $config['dropdown']['$overlay'] = true;
return \OOUI\InputWidget::getConfig( $config );
}
}
if ( $this->dataLocation ) {
$config['dataLocation'] = $this->dataLocation;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
}
public function getConfig( &$config ) {
$config['textinput'] = $this->config['textinput'];
$config['dropdowninput'] = $this->config['dropdowninput'];
+ $config['dropdowninput']['dropdown']['$overlay'] = true;
$config['or'] = $this->config['or'];
return parent::getConfig( $config );
}
if ( $this->validateTitle !== null ) {
$config['validateTitle'] = $this->validateTitle;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
}
protected function getJavaScriptClassName() {
return 'mw.widgets.UserInputWidget';
}
+
+ public function getConfig( &$config ) {
+ $config['$overlay'] = true;
+ return parent::getConfig( $config );
+ }
}
$config['placeholder'] = $this->inputPlaceholder;
}
+ $config['$overlay'] = true;
return parent::getConfig( $config );
}
* calendar uses relative positioning.
*/
mw.widgets.DateInputWidget = function MWWDateInputWidget( config ) {
- var placeholderDateFormat, mustBeAfter, mustBeBefore;
+ var placeholderDateFormat, mustBeAfter, mustBeBefore, $overlay;
// Config initialization
config = $.extend( {
.addClass( 'mw-widget-dateInputWidget' )
.append( this.$handle, this.textInput.$element, this.calendar.$element );
- // config.overlay is the selector to be used for config.$overlay, specified from PHP
- if ( config.overlay ) {
- config.$overlay = $( config.overlay );
- }
+ $overlay = config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay;
- if ( config.$overlay ) {
+ if ( $overlay ) {
this.calendar.setFloatableContainer( this.$element );
- config.$overlay.append( this.calendar.$element );
+ $overlay.append( this.calendar.$element );
// The text input and calendar are not in DOM order, so fix up focus transitions.
this.textInput.$input.on( 'keydown', function ( e ) {
} );
} );
capsulesWidget = new OO.ui.CapsuleMultiselectWidget( {
+ $overlay: true,
menu: {
items: capsulesOptions
}
use CommentStoreComment;
use Exception;
+use HashBagOStuff;
use InvalidArgumentException;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\IncompleteRevisionException;
use MediaWiki\Storage\MutableRevisionRecord;
use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
use MediaWikiTestCase;
use Revision;
use TestUserRegistry;
use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\TransactionProfiler;
use WikiPage;
use WikitextContent;
*/
class RevisionStoreDbTest extends MediaWikiTestCase {
+ /**
+ * @return LoadBalancer
+ */
+ private function getLoadBalancerMock( array $server ) {
+ $lb = $this->getMockBuilder( LoadBalancer::class )
+ ->setMethods( [ 'reallyOpenConnection' ] )
+ ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
+ ->getMock();
+
+ $lb->method( 'reallyOpenConnection' )->willReturnCallback(
+ function ( array $server, $dbNameOverride = false ) {
+ return $this->getDatabaseMock( $server );
+ }
+ );
+
+ return $lb;
+ }
+
+ /**
+ * @return Database
+ */
+ private function getDatabaseMock( array $params ) {
+ $db = $this->getMockBuilder( DatabaseSqlite::class )
+ ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
+ ->setConstructorArgs( [ $params ] )
+ ->getMock();
+
+ $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
+ $db->method( 'isOpen' )->willReturn( true );
+
+ return $db;
+ }
+
+ public function provideDomainCheck() {
+ yield [ false, 'test', '' ];
+ yield [ 'test', 'test', '' ];
+
+ yield [ false, 'test', 'foo_' ];
+ yield [ 'test-foo_', 'test', 'foo_' ];
+
+ yield [ false, 'dash-test', '' ];
+ yield [ 'dash-test', 'dash-test', '' ];
+
+ yield [ false, 'underscore_test', 'foo_' ];
+ yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
+ }
+
+ /**
+ * @dataProvider provideDomainCheck
+ * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId
+ */
+ public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
+ $this->setMwGlobals(
+ [
+ 'wgDBname' => $dbName,
+ 'wgDBprefix' => $dbPrefix,
+ ]
+ );
+
+ $loadBalancer = $this->getLoadBalancerMock(
+ [
+ 'host' => '*dummy*',
+ 'dbDirectory' => '*dummy*',
+ 'user' => 'test',
+ 'password' => 'test',
+ 'flags' => 0,
+ 'variables' => [],
+ 'schema' => '',
+ 'cliMode' => true,
+ 'agent' => '',
+ 'load' => 100,
+ 'profiler' => null,
+ 'trxProfiler' => new TransactionProfiler(),
+ 'connLogger' => new \Psr\Log\NullLogger(),
+ 'queryLogger' => new \Psr\Log\NullLogger(),
+ 'errorLogger' => new \Psr\Log\NullLogger(),
+ 'type' => 'test',
+ 'dbname' => $dbName,
+ 'tablePrefix' => $dbPrefix,
+ ]
+ );
+ $db = $loadBalancer->getConnection( DB_REPLICA );
+
+ $blobStore = $this->getMockBuilder( SqlBlobStore::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $store = new RevisionStore(
+ $loadBalancer,
+ $blobStore,
+ new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
+ $wikiId
+ );
+
+ $count = $store->countRevisionsByPageId( $db, 0 );
+
+ // Dummy check to make PhpUnit happy. We are really only interested in
+ // countRevisionsByPageId not failing due to the DB domain check.
+ $this->assertSame( 0, $count );
+ }
+
private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
$this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
$this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
'page', [ 'page_latest' => 0 ], [ 'page_id' => self::$repl['pageE'] ]
);
+ self::$repl['revF1'] = $this->addPage( 'F', "== Section 1 ==\nF 1.1\n\n== Section 2 ==\nF 1.2" );
+ self::$repl['pageF'] = Title::newFromText( 'ApiComparePagesTest F' )->getArticleId();
+
WikiPage::factory( Title::newFromText( 'ApiComparePagesTest C' ) )
->doDeleteArticleReal( 'Test for ApiComparePagesTest' );
],
false, true
],
+ 'Basic diff, test with sections' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest F',
+ 'fromsection' => 1,
+ 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?",
+ 'tosection' => 2,
+ ],
+ [
+ 'compare' => [
+ 'body' => '<tr><td colspan="2" class="diff-lineno" id="mw-diff-left-l1" >Line 1:</td>' . "\n"
+ . '<td colspan="2" class="diff-lineno">Line 1:</td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div>== Section <del class="diffchange diffchange-inline">1 </del>==</div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div>== Section <ins class="diffchange diffchange-inline">2 </ins>==</div></td></tr>' . "\n"
+ . '<tr><td class=\'diff-marker\'>−</td><td class=\'diff-deletedline\'><div><del class="diffchange diffchange-inline">F 1.1</del></div></td><td class=\'diff-marker\'>+</td><td class=\'diff-addedline\'><div><ins class="diffchange diffchange-inline">To text?</ins></div></td></tr>' . "\n",
+ 'fromid' => '{{REPL:pageF}}',
+ 'fromrevid' => '{{REPL:revF1}}',
+ 'fromns' => '0',
+ 'fromtitle' => 'ApiComparePagesTest F',
+ ]
+ ],
+ ],
'Diff with all props' => [
[
'fromrev' => '{{REPL:revB1}}',
[],
'compare-no-title',
],
+ 'Error, test with invalid from section ID' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest F',
+ 'fromsection' => 5,
+ 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?",
+ 'tosection' => 2,
+ ],
+ [],
+ 'nosuchfromsection',
+ ],
+ 'Error, test with invalid to section ID' => [
+ [
+ 'fromtitle' => 'ApiComparePagesTest F',
+ 'fromsection' => 1,
+ 'totext' => "== Section 1 ==\nTo text\n\n== Section 2 ==\nTo text?",
+ 'tosection' => 5,
+ ],
+ [],
+ 'nosuchtosection',
+ ],
'Error, Relative diff, no from revision' => [
[
'fromtext' => 'Foo',