* Marius Hoch
* Matěj Grabovský
* Matt Johnston
+* Matthew Flaschen
* Max Semenik
* Meno25
* MinuteElectron
proper, published library, which is now tagged as v1.0.0.
* A new message (defaulting to blank), 'editnotice-notext', can be shown to users
when they are editing if no edit notices apply to the page being edited.
+* (T94536) You can now make the sitenotice appear to logged-in users only by
+ editing MediaWiki:Anonnotice and replacing its content with "". Setting it to
+ "-" (default) will continue disable it and fallback to MediaWiki:Sitenotice.
==== External libraries ====
* MediaWiki now requires certain external libraries to be installed. In the past
wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
}
// Setup dummy Title, otherwise OutputPage::redirect will fail
- $title = Title::newFromText( NS_MAIN, 'REDIR' );
+ $title = Title::newFromText( 'REDIR', NS_MAIN );
$this->context->setTitle( $title );
$output = $this->context->getOutput();
// Since we only do this redir to change proto, always send a vary header
unset( $params[$idsKey] );
}
if ( isset( $params[$ofieldKey] ) ) {
- $params[] = $params[$ofieldKey];
+ $params[] = 'ofield=' . $params[$ofieldKey];
unset( $params[$ofieldKey] );
}
if ( isset( $params[$nfieldKey] ) ) {
- $params[] = $params[$nfieldKey];
+ $params[] = 'nfield=' . $params[$nfieldKey];
unset( $params[$nfieldKey] );
}
}
* Get a cached notice
*
* @param string $name Message name, or 'default' for $wgSiteNotice
- * @return string HTML fragment
+ * @return string|bool HTML fragment, or false to indicate that the caller
+ * should fall back to the next notice in its sequence
*/
private function getCachedNotice( $name ) {
global $wgRenderHashAppend, $parserMemc, $wgContLang;
}
} else {
$msg = $this->msg( $name )->inContentLanguage();
- if ( $msg->isDisabled() ) {
+ if ( $msg->isBlank() ) {
+ return '';
+ } elseif ( $msg->isDisabled() ) {
return false;
}
$notice = $msg->plain();
$siteNotice = $this->getCachedNotice( 'sitenotice' );
} else {
$anonNotice = $this->getCachedNotice( 'anonnotice' );
- if ( !$anonNotice ) {
+ if ( $anonNotice === false ) {
$siteNotice = $this->getCachedNotice( 'sitenotice' );
} else {
$siteNotice = $anonNotice;
}
}
- if ( !$siteNotice ) {
+ if ( $siteNotice === false ) {
$siteNotice = $this->getCachedNotice( 'default' );
}
}
$text = $msg->text();
} else {
global $wgContLang;
- $text = $wgContLang->getFormattedNsText(
+ $text = $wgContLang->getConverter()->convertNamespace(
MWNamespace::getSubject( $title->getNamespace() ) );
}
"resources/src/mediawiki.action",
"resources/src/mediawiki.api",
"resources/src/mediawiki.language",
+ "resources/src/mediawiki.messagePoster",
"resources/src/mediawiki.page",
"resources/src/mediawiki.special",
"resources/src/mediawiki.toolbar",
"feedback-error1": "Error: Unrecognized result from API",
"feedback-error2": "Error: Edit failed",
"feedback-error3": "Error: No response from API",
+ "feedback-error4": "Error: Unable to post to given feedback title",
"feedback-message": "Message:",
"feedback-subject": "Subject:",
"feedback-submit": "Submit",
"feedback-error1": "Error message, appears when an unknown error occurs submitting feedback",
"feedback-error2": "Error message, appears when we could not add feedback",
"feedback-error3": "Error message, appears when we lose our connection to the wiki",
+ "feedback-error4": "Error message, appears when mediawiki.feedback or one of its dependencies is misconfigured or there is a problem fetching one of the modules",
"feedback-message": "Label for a textarea; signature refers to a Wikitext signature.\n{{Identical|Message}}",
"feedback-subject": "Label for a text input\n{{Identical|Subject}}",
"feedback-submit": "Button label\n{{Identical|Submit}}",
"classes": [
"mw.Title",
"mw.Uri",
+ "mw.messagePoster.*",
"mw.notification",
"mw.Notification_",
"mw.user",
'mediawiki.Title',
'user.tokens',
),
+ 'targets' => array( 'desktop', 'mobile' ),
),
'mediawiki.api.login' => array(
'scripts' => 'resources/src/mediawiki.api/mediawiki.api.login.js',
'scripts' => 'resources/src/mediawiki/mediawiki.feedback.js',
'styles' => 'resources/src/mediawiki/mediawiki.feedback.css',
'dependencies' => array(
- 'mediawiki.api.edit',
+ 'mediawiki.messagePoster',
'mediawiki.Title',
'oojs-ui',
),
'feedback-error1',
'feedback-error2',
'feedback-error3',
+ 'feedback-error4',
'feedback-message',
'feedback-subject',
'feedback-submit',
),
'targets' => array( 'desktop', 'mobile' ),
),
+ 'mediawiki.messagePoster' => array(
+ 'scripts' => array(
+ 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.factory.js',
+ 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js',
+ ),
+ 'dependencies' => array(
+ 'oojs',
+ 'mediawiki.api',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
+ 'mediawiki.messagePoster.wikitext' => array(
+ 'scripts' => array(
+ 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js',
+ ),
+ 'dependencies' => array(
+ 'mediawiki.api.edit',
+ 'mediawiki.messagePoster',
+ ),
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.notification' => array(
'styles' => array(
'resources/src/mediawiki/mediawiki.notification.css',
--- /dev/null
+/*global OO*/
+( function ( mw ) {
+ /**
+ * This is the abstract base class for MessagePoster implementations.
+ *
+ * @abstract
+ * @class
+ *
+ * @constructor
+ * @param {mw.Title} title Title to post to
+ */
+ mw.messagePoster.MessagePoster = function MwMessagePoster() {};
+
+ OO.initClass( mw.messagePoster.MessagePoster );
+
+ /**
+ * Post a message (with subject and body) to a talk page.
+ *
+ * @param {string} subject Subject/topic title; plaintext only (no wikitext or HTML)
+ * @param {string} body Body, as wikitext. Signature code will automatically be added
+ * by MessagePosters that require one, unless the message already contains the string
+ * ~~~.
+ * @return {jQuery.Promise} Promise completing when the post succeeds or fails.
+ * For failure, will be rejected with three arguments:
+ *
+ * - primaryError - Primary error code. For a mw.Api failure,
+ * this should be 'api-fail'.
+ * - secondaryError - Secondary error code. For a mw.Api failure,
+ * this, should be mw.Api's code, e.g. 'http', 'ok-but-empty', or the error passed through
+ * from the server.
+ * - details - Further details about the error
+ *
+ * @localdoc
+ * The base class currently does nothing, but could be used for shared analytics or
+ * something.
+ */
+ mw.messagePoster.MessagePoster.prototype.post = function () {};
+}( mediaWiki ) );
--- /dev/null
+/*global OO*/
+( function ( mw, $ ) {
+ /**
+ * This is an implementation of MessagePoster for wikitext talk pages.
+ *
+ * @class mw.messagePoster.WikitextMessagePoster
+ * @extends mw.messagePoster.MessagePoster
+ *
+ * @constructor
+ * @param {mw.Title} title Wikitext page in a talk namespace, to post to
+ */
+ function WikitextMessagePoster( title ) {
+ this.api = new mw.Api();
+ this.title = title;
+ }
+
+ OO.inheritClass(
+ WikitextMessagePoster,
+ mw.messagePoster.MessagePoster
+ );
+
+ /**
+ * @inheritdoc
+ */
+ WikitextMessagePoster.prototype.post = function ( subject, body ) {
+ mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body );
+
+ // Add signature if needed
+ if ( body.indexOf( '~~~' ) === -1 ) {
+ body += '\n\n~~~~';
+ }
+
+ return this.api.newSection(
+ this.title,
+ subject,
+ body,
+ { redirect: true }
+ ).then( function ( resp, jqXHR ) {
+ if ( resp.edit.result === 'Success' ) {
+ return $.Deferred().resolve( resp, jqXHR );
+ } else {
+ // mediawiki.api.js checks for resp.error. Are there actually cases where the
+ // request fails, but it's not caught there?
+ return $.Deferred().reject( 'api-unexpected' );
+ }
+ }, function ( code, details ) {
+ return $.Deferred().reject( 'api-fail', code, details );
+ } ).promise();
+ };
+
+ mw.messagePoster.factory.register( 'wikitext', WikitextMessagePoster );
+ mw.messagePoster.WikitextMessagePoster = WikitextMessagePoster;
+}( mediaWiki, jQuery ) );
--- /dev/null
+/*global OO*/
+( function ( mw, $ ) {
+ /**
+ * This is a factory for MessagePoster objects, which allows a pluggable to way to script leaving a
+ * talk page message.
+ *
+ * @class mw.messagePoster.factory
+ * @singleton
+ */
+ function MwMessagePosterFactory() {
+ this.api = new mw.Api();
+ this.contentModelToClass = {};
+ }
+
+ OO.initClass( MwMessagePosterFactory );
+
+ // Note: This registration scheme is currently not compatible with LQT, since that doesn't
+ // have its own content model, just islqttalkpage. LQT pages will be passed to the wikitext
+ // MessagePoster.
+ /**
+ * Registers a MessagePoster subclass for a given content model.
+ *
+ * @param {string} contentModel Content model of pages this MessagePoster can post to
+ * @param {Function} messagePosterConstructor Constructor for MessagePoster
+ */
+ MwMessagePosterFactory.prototype.register = function ( contentModel, messagePosterConstructor ) {
+ if ( this.contentModelToClass[contentModel] !== undefined ) {
+ throw new Error( 'The content model \'' + contentModel + '\' is already registered.' );
+ }
+
+ this.contentModelToClass[contentModel] = messagePosterConstructor;
+ };
+
+ /**
+ * Unregisters a given content model
+ * This is exposed for testing and should not normally be needed.
+ *
+ * @param {string} contentModel Content model to unregister
+ */
+ MwMessagePosterFactory.prototype.unregister = function ( contentModel ) {
+ delete this.contentModelToClass[contentModel];
+ };
+
+ /**
+ * Creates a MessagePoster, given a title. A promise for this is returned.
+ * This works by determining the content model, then loading the corresponding
+ * module (which will register the MessagePoster class), and finally constructing it.
+ *
+ * This does not require the message and should be called as soon as possible, so it does the
+ * API and ResourceLoader requests in the background.
+ *
+ * @param {mw.Title} title Title that will be posted to
+ * @return {jQuery.Promise} Promise resolving to a mw.messagePoster.MessagePoster.
+ * For failure, rejected with up to three arguments:
+ *
+ * - errorCode Error code string
+ * - error Error explanation
+ * - details Further error details
+ */
+ MwMessagePosterFactory.prototype.create = function ( title ) {
+ var pageId, page, contentModel, moduleName,
+ factory = this;
+
+ return this.api.get( {
+ action: 'query',
+ prop: 'info',
+ indexpageids: 1,
+ titles: title.getPrefixedDb()
+ } ).then( function ( result ) {
+ if ( result.query.pageids.length > 0 ) {
+ pageId = result.query.pageids[0];
+ page = result.query.pages[pageId];
+
+ contentModel = page.contentmodel;
+ moduleName = 'mediawiki.messagePoster.' + contentModel;
+ return mw.loader.using( moduleName ).then( function () {
+ return factory.createForContentModel(
+ contentModel,
+ title
+ );
+ }, function () {
+ return $.Deferred().reject( 'failed-to-load-module', 'Failed to load the \'' + moduleName + '\' module' );
+ } );
+ } else {
+ return $.Deferred().reject( 'unexpected-response', 'Unexpected API response' );
+ }
+ }, function ( errorCode, details ) {
+ return $.Deferred().reject( 'content-model-query-failed', errorCode, details );
+ } ).promise();
+ };
+
+ /**
+ * Creates a MessagePoster instance, given a title and content model
+ *
+ * @private
+ *
+ * @param {string} contentModel Content model of title
+ * @param {mw.Title} title Title being posted to
+ * @return {mw.messagePoster.MessagePoster}
+ *
+ */
+ MwMessagePosterFactory.prototype.createForContentModel = function ( contentModel, title ) {
+ return new this.contentModelToClass[contentModel]( title );
+ };
+
+ mw.messagePoster = {
+ factory: new MwMessagePosterFactory()
+ };
+}( mediaWiki, jQuery ) );
* @class
* @constructor
* @param {Object} [config] Configuration object
- * @cfg {mw.Api} [api] if omitted, will just create a standard API
* @cfg {mw.Title} [title="Feedback"] The title of the page where you collect
* feedback.
* @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the
mw.Feedback = function MwFeedback( config ) {
config = config || {};
- this.api = config.api || new mw.Api();
this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title';
// Feedback page title
this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' );
+ this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle );
+
// Links
this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/';
this.bugsTaskListLink = config.bugsListLink || '//phabricator.wikimedia.org/maniphest/query/advanced';
case 'error1':
case 'error2':
case 'error3':
+ case 'error4':
dialogConfig = {
title: mw.msg( 'feedback-error-title' ),
message: mw.msg( 'feedback-' + status ),
* Modify the display form, and then open it, focusing interface on the subject.
*
* @param {Object} [contents] Prefilled contents for the feedback form.
- * @param {string} [contents.subject] The subject of the feedback
- * @param {string} [contents.message] The content of the feedback
+ * @param {string} [contents.subject] The subject of the feedback, as plaintext
+ * @param {string} [contents.message] The content of the feedback, as wikitext
*/
mw.Feedback.prototype.launch = function ( contents ) {
// Dialog
{
title: mw.msg( this.dialogTitleMessageKey ),
settings: {
- api: this.api,
+ messagePosterPromise: this.messagePosterPromise,
title: this.feedbackPageTitle,
dialogTitleMessageKey: this.dialogTitleMessageKey,
bugsTaskSubmissionLink: this.bugsTaskSubmissionLink,
this.feedbackMessageInput.setValue( data.contents.message );
this.status = '';
- this.api = settings.api;
+ this.messagePosterPromise = settings.messagePosterPromise;
this.setBugReportLink( settings.bugsTaskSubmissionLink );
this.feedbackPageTitle = settings.title;
this.feedbackPageName = settings.title.getNameText();
message = userAgentMessage + message;
}
- // Add signature if needed
- if ( message.indexOf( '~~~' ) === -1 ) {
- message += '\n\n~~~~';
- }
-
- // Post the message, resolving redirects
- this.pushPending();
- this.api.newSection(
- this.feedbackPageTitle,
- subject,
- message,
- { redirect: true }
- )
- .done( function ( result ) {
- if ( result.edit.result === 'Success' ) {
- fb.status = 'submitted';
- } else {
- fb.status = 'error1';
- }
- fb.popPending();
- fb.close();
- } )
- .fail( function ( code, result ) {
- if ( code === 'http' ) {
- fb.status = 'error3';
- // ajax request failed
- mw.log.warn( 'Feedback report failed with HTTP error: ' + result.textStatus );
- } else {
- fb.status = 'error2';
- mw.log.warn( 'Feedback report failed with API error: ' + code );
- }
- fb.popPending();
+ // Post the message
+ return this.messagePosterPromise.then( function ( poster ) {
+ return fb.postMessage( poster, subject, message );
+ }, function () {
+ fb.status = 'error4';
+ mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' );
+ } ).always( function () {
fb.close();
} );
}, this );
return mw.Feedback.Dialog.super.prototype.getActionProcess.call( this, action );
};
+ /**
+ * Posts the message
+ *
+ * @private
+ *
+ * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback
+ * @param {string} subject Subject of message
+ * @param {string} message Body of message
+ * @return {jQuery.Promise} Promise representing success of message posting action
+ */
+ mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) {
+ var fb = this;
+
+ return poster.post(
+ subject,
+ message
+ ).then( function () {
+ fb.status = 'submitted';
+ }, function ( mainCode, secondaryCode, details ) {
+ if ( mainCode === 'api-fail' ) {
+ if ( secondaryCode === 'http' ) {
+ fb.status = 'error3';
+ // ajax request failed
+ mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus );
+ } else {
+ fb.status = 'error2';
+ mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode );
+ }
+ } else {
+ fb.status = 'error1';
+ }
+ } );
+ };
+
/**
* @inheritdoc
*/
'tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
'mediawiki.api.parse',
'mediawiki.api.watch',
'mediawiki.jqueryMsg',
+ 'mediawiki.messagePoster',
'mediawiki.Title',
'mediawiki.toc',
'mediawiki.Uri',
--- /dev/null
+( function ( mw ) {
+ var TEST_MODEL = 'test-content-model';
+
+ QUnit.module( 'mediawiki.messagePoster', QUnit.newMwEnvironment( {
+ teardown: function () {
+ mw.messagePoster.factory.unregister( TEST_MODEL );
+ }
+ } ) );
+
+ QUnit.test( 'register', 2, function ( assert ) {
+ var testMessagePosterConstructor = function () {};
+
+ mw.messagePoster.factory.register( TEST_MODEL, testMessagePosterConstructor );
+ assert.strictEqual(
+ mw.messagePoster.factory.contentModelToClass[TEST_MODEL],
+ testMessagePosterConstructor,
+ 'Constructor is registered'
+ );
+
+ assert.throws(
+ function () {
+ mw.messagePoster.factory.register( TEST_MODEL, testMessagePosterConstructor );
+ },
+ new RegExp( 'The content model \'' + TEST_MODEL + '\' is already registered.' ),
+ 'Throws exception is same model is registered a second time'
+ );
+ } );
+}( mediaWiki ) );