'ChangesListSpecialPageStructuredFilters': Called to allow extensions to register
filters for pages inheriting from ChangesListSpecialPage (in core: RecentChanges,
RecentChangesLinked, and Watchlist). Generally, you will want to construct
-new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects. You can
-then either add them to existing ChangesListFilterGroup objects (accessed through
-$special->getFilterGroup), or create your own. If you create new groups, you
-must register them with $special->registerFilterGroup.
+new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects.
+
+When constructing them, you specify which group they belong to. You can reuse
+existing groups (accessed through $special->getFilterGroup), or create your own.
+If you create new groups, you must register them with $special->registerFilterGroup.
$special: ChangesListSpecialPage instance
'ChangeTagAfterDelete': Called after a change tag has been deleted (that is,
$title: The title the 'go' feature has decided to forward the user to
&$url: Initially null, hook subscribers can set this to specify the final url to redirect to
-'SpecialSearchNogomatch': Called when user clicked the "Go" button but the
-target doesn't exist.
+'SpecialSearchNogomatch': Called when the 'Go' feature is triggered (generally
+from autocomplete search other than the main bar on Special:Search) and the
+target doesn't exist. Full text search results are generated after this hook is
+called.
&$title: title object generated from the text entered by the user
'SpecialSearchPowerBox': The equivalent of SpecialSearchProfileForm for
*/
protected $priority;
+ const RESERVED_NAME_CHAR = '_';
+
/**
- * Create a new filter with the specified configuration.
+ * Creates a new filter with the specified configuration, and registers it to the
+ * specified group.
*
* It infers which UI (it can be either or both) to display the filter on based on
* which messages are provided.
*
* @param array $filterDefinition ChangesListFilter definition
*
- * $filterDefinition['name'] string Name of filter
+ * $filterDefinition['name'] string Name of filter; use lowercase with no
+ * punctuation
* $filterDefinition['cssClassSuffix'] string CSS class suffix, used to mark
* that a particular row belongs to this filter (when a row is included by the
* filter) (optional)
'ChangesListFilterGroup this filter belongs to' );
}
+ if ( strpos( $filterDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
+ throw new MWException( 'Filter names may not contain \'' .
+ self::RESERVED_NAME_CHAR .
+ '\'. Use the naming convention: \'lowercase\''
+ );
+ }
+
+ if ( $this->group->getFilter( $filterDefinition['name'] ) ) {
+ throw new MWException( 'Two filters in a group cannot have the ' .
+ "same name: '{$filterDefinition['name']}'" );
+ }
+
$this->name = $filterDefinition['name'];
if ( isset( $filterDefinition['cssClassSuffix'] ) ) {
const DEFAULT_PRIORITY = -100;
+ const RESERVED_NAME_CHAR = '_';
+
/**
* Create a new filter group with the specified configuration
*
* @param array $groupDefinition Configuration of group
- * * $groupDefinition['name'] string Group name
+ * * $groupDefinition['name'] string Group name; use camelCase with no punctuation
* * $groupDefinition['title'] string i18n key for title (optional, can be omitted
* * only if none of the filters in the group display in the structured UI)
* * $groupDefinition['type'] string A type constant from a subclass of this one
* * changes list entries are filtered out.
*/
public function __construct( array $groupDefinition ) {
+ if ( strpos( $groupDefinition['name'], self::RESERVED_NAME_CHAR ) !== false ) {
+ throw new MWException( 'Group names may not contain \'' .
+ self::RESERVED_NAME_CHAR .
+ '\'. Use the naming convention: \'camelCase\''
+ );
+ }
+
$this->name = $groupDefinition['name'];
if ( isset( $groupDefinition['title'] ) ) {
* Get filter by name
*
* @param string $name Filter name
- * @return ChangesListFilter Specified filter
+ * @return ChangesListFilter|null Specified filter, or null if it is not registered
*/
public function getFilter( $name ) {
- return $this->filters[$name];
+ return isset( $this->filters[$name] ) ? $this->filters[$name] : null;
}
/**
* @param Title $title
* @throws MWException
*/
- function setTitle( $title ) {
+ public function setTitle( $title ) {
if ( is_object( $title ) ) {
$this->title = $title;
} elseif ( is_null( $title ) ) {
/**
* @param int $id
*/
- function setID( $id ) {
+ public function setID( $id ) {
$this->id = $id;
}
/**
* @param string $ts
*/
- function setTimestamp( $ts ) {
+ public function setTimestamp( $ts ) {
# 2003-08-05T18:30:02Z
$this->timestamp = wfTimestamp( TS_MW, $ts );
}
/**
* @param string $user
*/
- function setUsername( $user ) {
+ public function setUsername( $user ) {
$this->user_text = $user;
}
/**
* @param User $user
*/
- function setUserObj( $user ) {
+ public function setUserObj( $user ) {
$this->userObj = $user;
}
/**
* @param string $ip
*/
- function setUserIP( $ip ) {
+ public function setUserIP( $ip ) {
$this->user_text = $ip;
}
/**
* @param string $model
*/
- function setModel( $model ) {
+ public function setModel( $model ) {
$this->model = $model;
}
/**
* @param string $format
*/
- function setFormat( $format ) {
+ public function setFormat( $format ) {
$this->format = $format;
}
/**
* @param string $text
*/
- function setText( $text ) {
+ public function setText( $text ) {
$this->text = $text;
}
/**
* @param string $text
*/
- function setComment( $text ) {
+ public function setComment( $text ) {
$this->comment = $text;
}
/**
* @param bool $minor
*/
- function setMinor( $minor ) {
+ public function setMinor( $minor ) {
$this->minor = (bool)$minor;
}
/**
* @param mixed $src
*/
- function setSrc( $src ) {
+ public function setSrc( $src ) {
$this->src = $src;
}
* @param string $src
* @param bool $isTemp
*/
- function setFileSrc( $src, $isTemp ) {
+ public function setFileSrc( $src, $isTemp ) {
$this->fileSrc = $src;
$this->fileIsTemp = $isTemp;
}
/**
* @param string $sha1base36
*/
- function setSha1Base36( $sha1base36 ) {
+ public function setSha1Base36( $sha1base36 ) {
$this->sha1base36 = $sha1base36;
}
/**
* @param string $filename
*/
- function setFilename( $filename ) {
+ public function setFilename( $filename ) {
$this->filename = $filename;
}
/**
* @param string $archiveName
*/
- function setArchiveName( $archiveName ) {
+ public function setArchiveName( $archiveName ) {
$this->archiveName = $archiveName;
}
/**
* @param int $size
*/
- function setSize( $size ) {
+ public function setSize( $size ) {
$this->size = intval( $size );
}
/**
* @param string $type
*/
- function setType( $type ) {
+ public function setType( $type ) {
$this->type = $type;
}
/**
* @param string $action
*/
- function setAction( $action ) {
+ public function setAction( $action ) {
$this->action = $action;
}
/**
* @param array $params
*/
- function setParams( $params ) {
+ public function setParams( $params ) {
$this->params = $params;
}
/**
* @return Title
*/
- function getTitle() {
+ public function getTitle() {
return $this->title;
}
/**
* @return int
*/
- function getID() {
+ public function getID() {
return $this->id;
}
/**
* @return string
*/
- function getTimestamp() {
+ public function getTimestamp() {
return $this->timestamp;
}
/**
* @return string
*/
- function getUser() {
+ public function getUser() {
return $this->user_text;
}
/**
* @return User
*/
- function getUserObj() {
+ public function getUserObj() {
return $this->userObj;
}
/**
* @return string
*/
- function getText() {
+ public function getText() {
return $this->text;
}
/**
* @return ContentHandler
*/
- function getContentHandler() {
+ public function getContentHandler() {
if ( is_null( $this->contentHandler ) ) {
$this->contentHandler = ContentHandler::getForModelID( $this->getModel() );
}
/**
* @return Content
*/
- function getContent() {
+ public function getContent() {
if ( is_null( $this->content ) ) {
$handler = $this->getContentHandler();
$this->content = $handler->unserializeContent( $this->text, $this->getFormat() );
/**
* @return string
*/
- function getModel() {
+ public function getModel() {
if ( is_null( $this->model ) ) {
$this->model = $this->getTitle()->getContentModel();
}
/**
* @return string
*/
- function getFormat() {
+ public function getFormat() {
if ( is_null( $this->format ) ) {
$this->format = $this->getContentHandler()->getDefaultFormat();
}
/**
* @return string
*/
- function getComment() {
+ public function getComment() {
return $this->comment;
}
/**
* @return bool
*/
- function getMinor() {
+ public function getMinor() {
return $this->minor;
}
/**
* @return mixed
*/
- function getSrc() {
+ public function getSrc() {
return $this->src;
}
/**
* @return bool|string
*/
- function getSha1() {
+ public function getSha1() {
if ( $this->sha1base36 ) {
return Wikimedia\base_convert( $this->sha1base36, 36, 16 );
}
/**
* @return string
*/
- function getFileSrc() {
+ public function getFileSrc() {
return $this->fileSrc;
}
/**
* @return bool
*/
- function isTempSrc() {
+ public function isTempSrc() {
return $this->isTemp;
}
/**
* @return mixed
*/
- function getFilename() {
+ public function getFilename() {
return $this->filename;
}
/**
* @return string
*/
- function getArchiveName() {
+ public function getArchiveName() {
return $this->archiveName;
}
/**
* @return mixed
*/
- function getSize() {
+ public function getSize() {
return $this->size;
}
/**
* @return string
*/
- function getType() {
+ public function getType() {
return $this->type;
}
/**
* @return string
*/
- function getAction() {
+ public function getAction() {
return $this->action;
}
/**
* @return string
*/
- function getParams() {
+ public function getParams() {
return $this->params;
}
/**
* @return bool
*/
- function importOldRevision() {
+ public function importOldRevision() {
$dbw = wfGetDB( DB_MASTER );
# Sneak a single revision into place
return true;
}
- function importLogItem() {
+ public function importLogItem() {
$dbw = wfGetDB( DB_MASTER );
$user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
/**
* @return bool
*/
- function importUpload() {
+ public function importUpload() {
# Construct a file
$archiveName = $this->getArchiveName();
if ( $archiveName ) {
/**
* @return bool|string
*/
- function downloadSource() {
+ public function downloadSource() {
if ( !$this->config->get( 'EnableUploads' ) ) {
return false;
}
'_MemCachedServers', 'wgDBserver', 'wgDBuser',
'wgDBpassword', 'wgUseInstantCommons', 'wgUpgradeKey', 'wgDefaultSkin',
'wgMetaNamespace', 'wgLogo', 'wgAuthenticationTokenVersion', 'wgPingback',
- '_Caches',
],
$db->getGlobalNames()
);
case 'db':
case 'memcached':
case 'accel':
- case 'none':
$cacheType = 'CACHE_' . strtoupper( $this->values['_MainCacheType'] );
break;
+ case 'none':
default:
- // If the user skipped the options page,
- // default to CACHE_ACCEL if available
- if ( count( $this->values['_Caches'] ) ) {
- $cacheType = 'CACHE_ACCEL';
- } else {
- $cacheType = 'CACHE_NONE';
- }
+ $cacheType = 'CACHE_NONE';
}
$mcservers = $this->buildMemcachedServerList();
*
* @param string $groupName Name of group
*
- * @return ChangesListFilterGroup
+ * @return ChangesListFilterGroup|null Group, or null if not registered
*/
public function getFilterGroup( $groupName ) {
- return $this->filterGroups[$groupName];
+ return isset( $this->filterGroups[$groupName] ) ?
+ $this->filterGroups[$groupName] :
+ null;
}
// Currently, this intentionally only includes filters that display
$out->redirect( $url );
return;
}
+ // No match. If it could plausibly be a title
+ // run the No go match hook.
+ $title = Title::newFromText( $term );
+ if ( !is_null( $title ) ) {
+ Hooks::run( 'SpecialSearchNogomatch', [ &$title ] );
+ }
}
$this->setupPage( $term );
"page_last": "last",
"histlegend": "Diff selection: Mark the radio boxes of the revisions to compare and hit enter or the button at the bottom.<br />\nLegend: <strong>({{int:cur}})</strong> = difference with latest revision, <strong>({{int:last}})</strong> = difference with preceding revision, <strong>{{int:minoreditletter}}</strong> = minor edit.",
"history-fieldset-title": "Browse history",
- "history-show-deleted": "Deleted only",
+ "history-show-deleted": "Revision deleted only",
"history_copyright": "-",
"histfirst": "oldest",
"histlast": "newest",
--- /dev/null
+jquery.ui.draggable.js
+* 71e11de2a3 Fix positioning error with draggable, revert and grid.
+ https://phabricator.wikimedia.org/T140965#2944610
+
+ https://bugs.jqueryui.com/ticket/4696
+
+
+jquery.ui.datepicker
+* 19531f3c23 Add translations in de-AT and de-CH
+
+
+themes/smoothness/jquery.ui.theme.css
+* 5e772e39dd Remove dark color from links inside dialogs
+ https://phabricator.wikimedia.org/T85857
+
+ Removed ".ui-widget-content a { color: #222222; }"
+ and ".ui-widget-header a { color: #222222; }"
+
+
+themes/smoothness/jquery.ui.core.css:
+* dc1c29f204 Collapse border in ui-helper-clearfix
+ https://phabricator.wikimedia.org/T73601
+
+ Backport of upstream change released in jQuery UI v1.10.1
+ - http://bugs.jqueryui.com/ticket/8442
+ - https://github.com/jquery/jquery-ui/commit/cb42ee7ccd
+++ /dev/null
-jquery.ui.theme.css
-* Removed ".ui-widget-content a { color: #222222; }" and
- ".ui-widget-header a { color: #222222; }" due to bug T85857.
.mw-rcfilters-ui-capsuleItemWidget {
background-color: #fff;
border-color: #979797;
+ margin: 0 0.6em 0 0;
color: #222;
// Background and color of the capsule widget need a bit
&.oo-ui-widget-enabled .oo-ui-capsuleMultiselectWidget-handle {
background-color: #f8f9fa;
- border: 1px solid #a2a9b1;
- min-height: 5.5em;
- padding: 0.75em;
+ border-radius: 2px 2px 0 0;
+ padding: 0.3em 0.6em 0.6em 0.6em;
+ margin-top: 1.6em;
+ }
+ .mw-rcfilters-ui-table {
+ margin-top: 0.3em;
}
- &-content-title {
+ &-wrapper-content-title {
font-weight: bold;
color: #54595d;
}
&-search {
max-width: none;
- margin-top: -0.5em;
+ margin-top: -1px;
input {
// We need to reiterate the directionality
.mw-rcfilters-ui-filtersListWidget {
&-title {
font-size: 1.2em;
- padding: 0.75em;
+ padding: 0.75em 0.5em;
// TODO: Unify colors with official design palette
color: #54595d;
}
&-highlight {
width: 1em;
vertical-align: middle;
+ // Using the same padding that the filter item
+ // uses, so the button is aligned with the highlight
+ // buttons for the filters
+ padding-right: 0.5em;
}
&-title {
'MockDjVuHandler' => "$testDir/phpunit/mocks/media/MockDjVuHandler.php",
'MockOggHandler' => "$testDir/phpunit/mocks/media/MockOggHandler.php",
'MockMediaHandlerFactory' => "$testDir/phpunit/mocks/media/MockMediaHandlerFactory.php",
+ 'MockChangesListFilter' => "$testDir/phpunit/mocks/MockChangesListFilter.php",
+ 'MockChangesListFilterGroup' => "$testDir/phpunit/mocks/MockChangesListFilterGroup.php",
'MockWebRequest' => "$testDir/phpunit/mocks/MockWebRequest.php",
'MediaWiki\\Session\\DummySessionBackend'
=> "$testDir/phpunit/mocks/session/DummySessionBackend.php",
);
}
- public function testAutoPriorities() {
- $group = new ChangesListBooleanFilterGroup( [
- 'name' => 'groupName',
- 'priority' => 1,
- 'filters' => [
- [ 'name' => 'hidefoo', 'default' => false, ],
- [ 'name' => 'hidebar', 'default' => false, ],
- [ 'name' => 'hidebaz', 'default' => false, ],
- ],
- ] );
-
- $filters = $group->getFilters();
- $this->assertEquals(
- [
- -2,
- -3,
- -4,
- ],
- array_map(
- function ( $f ) {
- return $f->getPriority();
- },
- array_values( $filters )
- )
- );
- }
-
public function testGetJsData() {
$definition = [
'name' => 'some-group',
);
}
- /**
- * @expectedException MWException
- * @expectedExceptionMessage Supersets can only be defined for filters in the same group
- */
- public function testSetAsSupersetOf() {
- $groupA = new ChangesListBooleanFilterGroup( [
- 'name' => 'groupA',
- 'priority' => 2,
- 'filters' => [
- [
- 'name' => 'foo',
- 'default' => false,
- ],
- [
- 'name' => 'bar',
- 'default' => false,
- ]
- ],
- ] );
-
- $groupB = new ChangesListBooleanFilterGroup( [
- 'name' => 'groupB',
- 'priority' => 3,
- 'filters' => [
- [
- 'name' => 'baz',
- 'default' => true,
- ],
- ],
- ] );
-
- $foo = TestingAccessWrapper::newFromObject( $groupA->getFilter( 'foo' ) );
-
- $bar = $groupA->getFilter( 'bar' );
-
- $baz = $groupB->getFilter( 'baz' );
-
- $foo->setAsSupersetOf( $bar );
- $this->assertArrayEquals( [
- [
- 'group' => 'groupA',
- 'filter' => 'bar',
- ],
- ],
- $foo->subsetFilters,
- /** ordered= */ false,
- /** named= */ true
- );
-
- $foo->setAsSupersetOf( $baz, 'some-message' );
- }
-
public function testIsFeatureAvailableOnStructuredUi() {
$specialPage = $this->getMockBuilder( 'ChangesListSpecialPage' )
->setConstructorArgs( [
--- /dev/null
+<?php
+
+/**
+ * @covers ChangesListFilterGroup
+ */
+class ChangesListFilterGroupTest extends MediaWikiTestCase {
+ // @codingStandardsIgnoreStart
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Group names may not contain '_'. Use the naming convention: 'camelCase'
+ */
+ // @codingStandardsIgnoreEnd
+ public function testReservedCharacter() {
+ new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'group_name',
+ 'priority' => 1,
+ 'filters' => [],
+ ]
+ );
+ }
+
+ public function testAutoPriorities() {
+ $group = new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'groupName',
+ 'isFullCoverage' => true,
+ 'priority' => 1,
+ 'filters' => [
+ [ 'name' => 'hidefoo' ],
+ [ 'name' => 'hidebar' ],
+ [ 'name' => 'hidebaz' ],
+ ],
+ ]
+ );
+
+ $filters = $group->getFilters();
+ $this->assertEquals(
+ [
+ -2,
+ -3,
+ -4,
+ ],
+ array_map(
+ function ( $f ) {
+ return $f->getPriority();
+ },
+ array_values( $filters )
+ )
+ );
+ }
+
+ // Get without warnings
+ public function testGetFilter() {
+ $group = new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'groupName',
+ 'isFullCoverage' => true,
+ 'priority' => 1,
+ 'filters' => [
+ [ 'name' => 'foo' ],
+ ],
+ ]
+ );
+
+ $this->assertEquals(
+ 'foo',
+ $group->getFilter( 'foo' )->getName()
+ );
+
+ $this->assertEquals(
+ null,
+ $group->getFilter( 'bar' )
+ );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * @covers ChangesListFilter
+ */
+class ChangesListFilterTest extends MediaWikiTestCase {
+ protected $group;
+
+ public function setUp() {
+ $this->group = $this->getGroup( [ 'name' => 'group' ] );
+
+ parent::setUp();
+ }
+
+ protected function getGroup( $groupDefinition ) {
+ return new MockChangesListFilterGroup(
+ $groupDefinition + [
+ 'isFullCoverage' => true,
+ 'type' => 'some_type',
+ 'name' => 'group',
+ 'filters' => [],
+ ]
+ );
+
+ }
+
+ // @codingStandardsIgnoreStart
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Filter names may not contain '_'. Use the naming convention: 'lowercase'
+ */
+ // @codingStandardsIgnoreEnd
+ public function testReservedCharacter() {
+ $filter = new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'some_name',
+ 'priority' => 1,
+ ]
+ );
+ }
+
+ // @codingStandardsIgnoreStart
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Two filters in a group cannot have the same name: 'somename'
+ */
+ // @codingStandardsIgnoreEnd
+ public function testDuplicateName() {
+ new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'somename',
+ 'priority' => 1,
+ ]
+ );
+
+ new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'somename',
+ 'priority' => 2,
+ ]
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Supersets can only be defined for filters in the same group
+ */
+ public function testSetAsSupersetOf() {
+ $groupA = $this->getGroup(
+ [
+ 'name' => 'groupA',
+ 'filters' => [
+ [
+ 'name' => 'foo',
+ ],
+ [
+ 'name' => 'bar',
+ ]
+ ],
+ ]
+ );
+
+ $groupB = $this->getGroup(
+ [
+ 'name' => 'groupB',
+ 'filters' => [
+ [
+ 'name' => 'baz',
+ ],
+ ],
+ ]
+ );
+
+ $foo = TestingAccessWrapper::newFromObject( $groupA->getFilter( 'foo' ) );
+
+ $bar = $groupA->getFilter( 'bar' );
+
+ $baz = $groupB->getFilter( 'baz' );
+
+ $foo->setAsSupersetOf( $bar );
+ $this->assertArrayEquals( [
+ [
+ 'group' => 'groupA',
+ 'filter' => 'bar',
+ ],
+ ],
+ $foo->subsetFilters,
+ /** ordered= */ false,
+ /** named= */ true
+ );
+
+ $foo->setAsSupersetOf( $baz );
+ }
+}
--- /dev/null
+<?php
+
+class MockChangesListFilter extends ChangesListFilter {
+ public function displaysOnUnstructuredUi( ChangesListSpecialPage $specialPage ) {
+ throw new MWException(
+ 'Not implemented: If the test relies on this, put it one of the ' .
+ 'subclasses\' tests (e.g. ChangesListBooleanFilterTest) ' .
+ 'instead of testing the abstract class'
+ );
+ }
+}
--- /dev/null
+<?php
+
+class MockChangesListFilterGroup extends ChangesListFilterGroup {
+ public function createFilter( array $filterDefinition ) {
+ return new MockChangesListFilter( $filterDefinition );
+ }
+
+ public function registerFilter( MockChangesListFilter $filter ) {
+ $this->filters[$filter->getName()] = $filter;
+ }
+
+ public function isPerGroupRequestParameter() {
+ throw new MWException(
+ 'Not implemented: If the test relies on this, put it one of the ' .
+ 'subclasses\' tests (e.g. ChangesListBooleanFilterGroupTest) ' .
+ 'instead of testing the abstract class'
+ );
+ }
+}