* Parser test suite files to be run by parserTests.php when no specific
* filename is passed to it.
*
- * Extensions may add their own tests to this array, or site-local tests
- * may be added via LocalSettings.php
+ * Extensions using extension.json will have any *.txt file in a
+ * tests/parser/ directory automatically run.
+ *
+ * Core tests can be added to ParserTestRunner::$coreTestFiles.
*
* Use full paths.
+ *
+ * @deprecated since 1.30
*/
-$wgParserTestFiles = [
- "$IP/tests/parser/parserTests.txt",
- "$IP/tests/parser/extraParserTests.txt"
-];
+$wgParserTestFiles = [];
/**
* Allow running of javascript test suites via [[Special:JavaScriptTest]] (such as QUnit).
*/
$wgStructuredChangeFiltersEnableExperimentalViews = false;
+/**
+ * Whether to allow users to use the experimental live update feature in the new RecentChanges UI
+ */
+$wgStructuredChangeFiltersEnableLiveUpdate = false;
+
/**
* Use new page patrolling to check new pages on Special:Newpages
*/
$ret = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $text );
// Strip tags and decode.
- $ret = html_entity_decode( strip_tags( $ret ), ENT_QUOTES | ENT_HTML5 );
+ $ret = Sanitizer::stripAllTags( $ret );
return $ret;
}
// customizations, and make a basic attempt to turn markup into text.
$msg = $this->getMessageObject()->inLanguage( 'en' )->useDatabase( false )->text();
$msg = preg_replace( '!</?(var|kbd|samp|code)>!', '"', $msg );
- $msg = html_entity_decode( strip_tags( $msg ), ENT_QUOTES | ENT_HTML5 );
+ $msg = Sanitizer::stripAllTags( $msg );
parent::__construct( $msg, $code, $previous );
}
$text = preg_replace( '/<a href="(.*?)".*?>(.*?)<\/a>/', '$2 <$1>', $text );
- return html_entity_decode( strip_tags( $text ), ENT_QUOTES );
+ return Sanitizer::stripAllTags( $text );
}
/**
* @param string $subpage
*/
public function execute( $subpage ) {
- global $wgStructuredChangeFiltersEnableSaving,
- $wgStructuredChangeFiltersEnableExperimentalViews;
-
// Backwards-compatibility: redirect to new feed URLs
$feedFormat = $this->getRequest()->getVal( 'feed' );
if ( !$this->including() && $feedFormat ) {
)
);
+ $experimentalStructuredChangeFilters =
+ $this->getConfig()->get( 'StructuredChangeFiltersEnableExperimentalViews' );
+
$out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] );
$out->addJsConfigVars(
'wgStructuredChangeFiltersEnableSaving',
- $wgStructuredChangeFiltersEnableSaving
+ $this->getConfig()->get( 'StructuredChangeFiltersEnableSaving' )
);
$out->addJsConfigVars(
'wgStructuredChangeFiltersEnableExperimentalViews',
- $wgStructuredChangeFiltersEnableExperimentalViews
+ $experimentalStructuredChangeFilters
);
$out->addJsConfigVars(
- 'wgRCFiltersChangeTags',
- $this->buildChangeTagList()
+ 'wgStructuredChangeFiltersEnableLiveUpdate',
+ $this->getConfig()->get( 'StructuredChangeFiltersEnableLiveUpdate' )
);
+ if ( $experimentalStructuredChangeFilters ) {
+ $out->addJsConfigVars(
+ 'wgRCFiltersChangeTags',
+ $this->buildChangeTagList()
+ );
+ }
}
}
* @return Array Tag data
*/
protected function buildChangeTagList() {
- function stripAllHtml( $input ) {
- return trim( html_entity_decode( strip_tags( $input ) ) );
- }
-
$explicitlyDefinedTags = array_fill_keys( ChangeTags::listExplicitlyDefinedTags(), 0 );
$softwareActivatedTags = array_fill_keys( ChangeTags::listSoftwareActivatedTags(), 0 );
$tagStats = ChangeTags::tagUsageStatistics();
$result[] = [
'name' => $tagName,
- 'label' => stripAllHtml( ChangeTags::tagDescription( $tagName, $this->getContext() ) ),
- 'description' => $desc ? stripAllHtml( $desc->parse() ) : '',
+ 'label' => Sanitizer::stripAllTags(
+ ChangeTags::tagDescription( $tagName, $this->getContext() )
+ ),
+ 'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
'hits' => $hits,
];
"rcfilters-view-namespaces-tooltip": "Filter results by namespace",
"rcfilters-view-tags-tooltip": "Filter results using edit tags",
"rcfilters-view-return-to-default-tooltip": "Return to main filter menu",
+ "rcfilters-liveupdates-button": "Live updates",
"rcnotefrom": "Below {{PLURAL:$5|is the change|are the changes}} since <strong>$3, $4</strong> (up to <strong>$1</strong> shown).",
"rclistfromreset": "Reset date selection",
"rclistfrom": "Show new changes starting from $2, $3",
"rcfilters-view-namespaces-tooltip": "Tooltip for the button that loads the namespace view in [[Special:RecentChanges]]",
"rcfilters-view-tags-tooltip": "Tooltip for the button that loads the tags view in [[Special:RecentChanges]]",
"rcfilters-view-return-to-default-tooltip": "Tooltip for the button that returns to the default filter view in [[Special:RecentChanges]]",
+ "rcfilters-liveupdates-button": "Label for the button to enable or disable live updates on [[Special:RecentChanges]]",
"rcnotefrom": "This message is displayed at [[Special:RecentChanges]] when viewing recentchanges from some specific time.\n\nThe corresponding message is {{msg-mw|Rclistfrom}}.\n\nParameters:\n* $1 - the maximum number of changes that are displayed\n* $2 - (Optional) a date and time\n* $3 - a date\n* $4 - a time\n* $5 - Number of changes are displayed, for use with PLURAL",
"rclistfromreset": "Used on [[Special:RecentChanges]] to reset a selection of a certain date range.",
"rclistfrom": "Used on [[Special:RecentChanges]]. Parameters:\n* $1 - (Currently not use) date and time. The date and the time adds to the rclistfrom description.\n* $2 - time. The time adds to the rclistfrom link description (with split of date and time).\n* $3 - date. The date adds to the rclistfrom link description (with split of date and time).\n\nThe corresponding message is {{msg-mw|Rcnotefrom}}.",
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterItemHighlightButton.js',
'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.HighlightColorPickerWidget.js',
+ 'resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.HighlightColors.js',
'resources/src/mediawiki.rcfilters/mw.rcfilters.init.js',
],
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SavedLinksListItemWidget.less',
'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.SaveFiltersPopupButtonWidget.less',
+ 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.LiveUpdateButtonWidget.less',
],
'skinStyles' => [
'monobook' => [
'rcfilters-view-namespaces-tooltip',
'rcfilters-view-tags-tooltip',
'rcfilters-view-return-to-default-tooltip',
+ 'rcfilters-liveupdates-button',
'blanknamespace',
'namespaces',
'invert',
'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-content',
'oojs-ui.styles.icons-layout',
+ 'oojs-ui.styles.icons-media',
],
],
'mediawiki.special' => [
/**
* @event update
* @param {jQuery|string} changesListContent
+ * @param {jQuery} $fieldset
*
* The list of change is now up to date
*/
this._trackHighlight( 'clear', filterName );
};
+ /**
+ * Enable or disable live updates.
+ * @param {boolean} enable True to enable, false to disable
+ */
+ mw.rcfilters.Controller.prototype.toggleLiveUpdate = function ( enable ) {
+ if ( enable && !this.liveUpdateTimeout ) {
+ this._scheduleLiveUpdate();
+ } else if ( !enable && this.liveUpdateTimeout ) {
+ clearTimeout( this.liveUpdateTimeout );
+ this.liveUpdateTimeout = null;
+ }
+ };
+
+ /**
+ * Set a timeout for the next live update.
+ * @private
+ */
+ mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () {
+ this.liveUpdateTimeout = setTimeout( this._doLiveUpdate.bind( this ), 3000 );
+ };
+
+ /**
+ * Perform a live update.
+ * @private
+ */
+ mw.rcfilters.Controller.prototype._doLiveUpdate = function () {
+ var controller = this;
+ this.updateChangesList( {}, true )
+ .always( function () {
+ if ( controller.liveUpdateTimeout ) {
+ // Live update was not disabled in the meantime
+ controller._scheduleLiveUpdate();
+ }
+ } );
+ };
+
/**
* Save the current model state as a saved query
*
* Update the list of changes and notify the model
*
* @param {Object} [params] Extra parameters to add to the API call
+ * @param {boolean} [isLiveUpdate] Don't update the URL or invalidate the changes list
+ * @return {jQuery.Promise} Promise that is resolved when the update is complete
*/
- mw.rcfilters.Controller.prototype.updateChangesList = function ( params ) {
- this._updateURL( params );
- this.changesListModel.invalidate();
- this._fetchChangesList()
+ mw.rcfilters.Controller.prototype.updateChangesList = function ( params, isLiveUpdate ) {
+ if ( !isLiveUpdate ) {
+ this._updateURL( params );
+ this.changesListModel.invalidate();
+ }
+ return this._fetchChangesList()
.then(
// Success
function ( pieces ) {
&-viewToggleButtons {
margin-top: 1em;
}
+
+ &-bottom {
+ margin-top: 1em;
+ }
}
--- /dev/null
+.mw-rcfilters-ui-liveUpdateButtonWidget {
+ &.oo-ui-toggleWidget-on {
+ position: relative;
+ overflow: hidden;
+ &:after {
+ content: '';
+ mix-blend-mode: screen;
+ position: absolute;
+ width: 1.875em;
+ height: 1.875em;
+ top: 1.875em / 4;
+ left: 0.46875em;
+ background: rgba( 51, 102, 204, 0.5 );
+ border-radius: 100%;
+ transform-origin: 50% 50%;
+ opacity: 0;
+ animation: ripple 1.2s ease-out infinite;
+ animation-delay: 1s;
+ }
+ }
+}
+
+@keyframes ripple {
+ 0%,
+ 35% {
+ transform: scale( 0 );
+ opacity: 1;
+ }
+ 50% {
+ transform: scale( 1.5 );
+ opacity: 0.8;
+ }
+ 100% {
+ opacity: 0;
+ transform: scale( 4 );
+ }
+}
* @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
*/
mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, savedQueriesModel, config ) {
+ var $bottom;
config = config || {};
// Parent
{ $overlay: this.$overlay }
);
+ this.liveUpdateButton = new mw.rcfilters.ui.LiveUpdateButtonWidget(
+ this.controller
+ );
+
// Initialize
this.$element
.addClass( 'mw-rcfilters-ui-filterWrapperWidget' );
}
+ $bottom = $( '<div>' )
+ .addClass( 'mw-rcfilters-ui-filterWrapperWidget-bottom' );
+
+ if ( mw.config.get( 'wgStructuredChangeFiltersEnableLiveUpdate' ) ) {
+ $bottom.append( this.liveUpdateButton.$element );
+ }
+
this.$element.append(
- this.filterTagWidget.$element
+ this.filterTagWidget.$element,
+ $bottom
);
};
--- /dev/null
+( function ( mw ) {
+ /**
+ * Widget for toggling live updates
+ *
+ * @extends OO.ui.ToggleButtonWidget
+ *
+ * @constructor
+ * @param {mw.rcfilters.Controller} controller
+ * @param {Object} config Configuration object
+ */
+ mw.rcfilters.ui.LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, config ) {
+ config = config || {};
+
+ // Parent
+ mw.rcfilters.ui.LiveUpdateButtonWidget.parent.call( this, $.extend( {
+ icon: 'play',
+ label: mw.message( 'rcfilters-liveupdates-button' ).text()
+ } ), config );
+
+ this.controller = controller;
+
+ // Events
+ this.connect( this, { change: 'onChange' } );
+
+ this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
+ };
+
+ /* Initialization */
+
+ OO.inheritClass( mw.rcfilters.ui.LiveUpdateButtonWidget, OO.ui.ToggleButtonWidget );
+
+ /* Methods */
+
+ /**
+ * Respond to the button being toggled.
+ * @param {boolean} enable Whether the button is now pressed/enabled
+ */
+ mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onChange = function ( enable ) {
+ this.controller.toggleLiveUpdate( enable );
+ };
+
+}( mediaWiki ) );
* @ingroup Testing
*/
class ParserTestRunner {
+
+ /**
+ * MediaWiki core parser test files, paths
+ * will be prefixed with __DIR__ . '/'
+ *
+ * @var array
+ */
+ private static $coreTestFiles = [
+ 'parserTests.txt',
+ 'extraParserTests.txt',
+ ];
+
/**
* @var bool $useTemporaryTables Use temporary tables for the temporary database
*/
}
}
+ /**
+ * Get list of filenames to extension and core parser tests
+ *
+ * @return array
+ */
+ public static function getParserTestFiles() {
+ global $wgParserTestFiles;
+
+ // Add core test files
+ $files = array_map( function( $item ) {
+ return __DIR__ . "/$item";
+ }, self::$coreTestFiles );
+
+ // Plus legacy global files
+ $files = array_merge( $files, $wgParserTestFiles );
+
+ // Auto-discover extension parser tests
+ $registry = ExtensionRegistry::getInstance();
+ foreach ( $registry->getAllThings() as $info ) {
+ $dir = dirname( $info['path'] ) . '/tests/parser';
+ if ( !file_exists( $dir ) ) {
+ continue;
+ }
+ $dirIterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator( $dir )
+ );
+ foreach ( $dirIterator as $fileInfo ) {
+ /** @var SplFileInfo $fileInfo */
+ if ( substr( $fileInfo->getFilename(), -4 ) === '.txt' ) {
+ $files[] = $fileInfo->getPathname();
+ }
+ }
+ }
+
+ return array_unique( $files );
+ }
+
public function getRecorder() {
return $this->recorder;
}
}
public function execute() {
- global $wgParserTestFiles, $wgDBtype;
+ global $wgDBtype;
// Cases of weird db corruption were encountered when running tests on earlyish
// versions of SQLite
}
// Default parser tests and any set from extensions or local config
- $files = $this->getOption( 'file', $wgParserTestFiles );
+ $files = $this->getOption( 'file', ParserTestRunner::getParserTestFiles() );
$norm = $this->hasOption( 'norm' ) ? explode( ',', $this->getOption( 'norm' ) ) : [];
if ( is_string( $flags ) ) {
$flags = self::CORE_ONLY;
}
- global $wgParserTestFiles, $IP;
+ global $IP;
$mwTestDir = $IP . '/tests/';
$filesToTest = [];
# Filter out .txt files
- foreach ( $wgParserTestFiles as $parserTestFile ) {
+ $files = ParserTestRunner::getParserTestFiles();
+ foreach ( $files as $parserTestFile ) {
$isCore = ( 0 === strpos( $parserTestFile, $mwTestDir ) );
if ( $isCore && $wantsCore ) {