diff --git a/RELEASE-NOTES-1.30 b/RELEASE-NOTES-1.30 index 343c296c81d..cd800dab42f 100644 --- a/RELEASE-NOTES-1.30 +++ b/RELEASE-NOTES-1.30 @@ -99,6 +99,13 @@ changes to languages because of Phabricator reports. or wikilinks. * (T163966) Page moves are now counted as edits for the purposes of autopromotion, i.e., they increment the user_editcount field in the database. +* Two new hooks, LogEventsListLineEnding and NewPagesLineEnding were added for + manipulating Special:Log and Special:NewPages lines. +* The OldChangesListRecentChangesLine, EnhancedChangesListModifyLineData, + PageHistoryLineEnding, ContributionsLineEnding and DeletedContributionsLineEnding + hooks have an additional parameter, for manipulating HTML data attributes of + RC/history lines. EnhancedChangesListModifyBlockLineData can do that via the + $data['attribs'] subarray. == Compatibility == MediaWiki 1.30 requires PHP 5.5.9 or later. There is experimental support for diff --git a/docs/hooks.txt b/docs/hooks.txt index 0e8b50829de..3d310c3508f 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1155,6 +1155,9 @@ $page: SpecialPage object for contributions &$ret: the HTML line $row: the DB row for this line &$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks': Change tool links above Special:Contributions $id: User identifier @@ -1200,6 +1203,9 @@ $page: SpecialPage object for DeletedContributions &$ret: the HTML line $row: the DB row for this line &$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'DifferenceEngineAfterLoadNewText': called in DifferenceEngine::loadNewText() after the new revision's content has been loaded into the class member variable @@ -1512,6 +1518,9 @@ $changesList: EnhancedChangesList object $block: An array of RecentChange objects in that block $rc: The RecentChange object for this line &$classes: An array of classes to change +&$attribs: associative array of other HTML attributes for the element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'EnhancedChangesListModifyBlockLineData': to alter data used to build a non-grouped recent change line in EnhancedChangesList. @@ -1999,6 +2008,16 @@ $file: the File object or false if broken link &$attribs: the attributes to be applied &$ret: the value to return if your hook returns false +'LogEventsListLineEnding': Called before a Special:Log line is finished +$page: the LogEventsList object +&$ret: the HTML line +$entry: the DatabaseLogEntry object for this row +&$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). + + 'HtmlPageLinkRendererBegin': Used when generating internal and interwiki links in LinkRenderer, before processing starts. Return false to skip default @@ -2284,6 +2303,16 @@ $title: the diff page title (nullable) $old: the ?old= param value from the url $new: the ?new= param value from the url +'NewPagesLineEnding': Called before a NewPages line is finished. +$page: the SpecialNewPages object +&$ret: the HTML line +$row: the database row for this page (the recentchanges record and a few extras - see + NewPagesPager::getQueryInfo) +&$classes: the classes to add to the surrounding
  • +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). + 'NewRevisionFromEditComplete': Called when a revision was inserted due to an edit. $wikiPage: the WikiPage edited @@ -2296,7 +2325,10 @@ return false to omit the line from RecentChanges and Watchlist special pages. &$changeslist: The OldChangesList instance. &$s: HTML of the form "
  • ...
  • " containing one RC entry. $rc: The RecentChange object. -&$classes: array of css classes for the
  • element +&$classes: array of css classes for the
  • element. +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'OpenSearchUrls': Called when constructing the OpenSearch description XML. Hooks can alter or append to the array of URLs for search & suggestion formats. @@ -2404,6 +2436,9 @@ $historyAction: the action object &$row: the revision row for this line &$s: the string representing this parsed line &$classes: array containing the
  • element classes +&$attribs: associative array of other HTML attributes for the
  • element. + Currently only data attributes reserved to MediaWiki are allowed + (see Sanitizer::isReservedDataAttribute). 'PageHistoryPager::doBatchLookups': Called after the pager query was run, before any output is generated, to allow batch lookups for prefetching information diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index c6ccf31e631..2090d90cb74 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -203,6 +203,38 @@ function wfArrayDiff2_cmp( $a, $b ) { } } +/** + * Like array_filter with ARRAY_FILTER_USE_BOTH, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array value and key (in that order) and + * should return a bool which will determine whether the array element is kept. + * @return array + */ +function wfArrayFilter( array $arr, callable $callback ) { + if ( defined( 'ARRAY_FILTER_USE_BOTH' ) ) { + return array_filter( $arr, $callback, ARRAY_FILTER_USE_BOTH ); + } + $filteredKeys = array_filter( array_keys( $arr ), function ( $key ) use ( $arr, $callback ) { + return call_user_func( $callback, $arr[$key], $key ); + } ); + return array_intersect_key( $arr, array_fill_keys( $filteredKeys, true ) ); +} + +/** + * Like array_filter with ARRAY_FILTER_USE_KEY, but works pre-5.6. + * + * @param array $arr + * @param callable $callback Will be called with the array key and should return a bool which + * will determine whether the array element is kept. + * @return array + */ +function wfArrayFilterByKey( array $arr, callable $callback ) { + return wfArrayFilter( $arr, function ( $val, $key ) use ( $callback ) { + return call_user_func( $callback, $key ); + } ); +} + /** * Appends to second array if $value differs from that in $default * diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index c4883ba289e..5aaa3ed1066 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -782,15 +782,12 @@ class Sanitizer { # Allow any attribute beginning with "data-" # However: - # * data-ooui is reserved for ooui - # * data-mw and data-parsoid are reserved for parsoid - # * data-mw- is reserved for extensions (or core) if - # they need to communicate some data to the client and want to be - # sure that it isn't coming from an untrusted user. + # * Disallow data attributes used by MediaWiki code # * Ensure that the attribute is not namespaced by banning # colons. - if ( !preg_match( '/^data-(?!ooui|mw|parsoid)[^:]*$/i', $attribute ) + if ( !preg_match( '/^data-[^:]*$/i', $attribute ) && !isset( $whitelist[$attribute] ) + || self::isReservedDataAttribute( $attribute ) ) { continue; } @@ -858,6 +855,24 @@ class Sanitizer { return $out; } + /** + * Given an attribute name, checks whether it is a reserved data attribute + * (such as data-mw-foo) which is unavailable to user-generated HTML so MediaWiki + * core and extension code can safely use it to communicate with frontend code. + * @param string $attr Attribute name. + * @return bool + */ + public static function isReservedDataAttribute( $attr ) { + // data-ooui is reserved for ooui. + // data-mw and data-parsoid are reserved for parsoid. + // data-mw- is reserved for extensions (or core) if + // they need to communicate some data to the client and want to be + // sure that it isn't coming from an untrusted user. + // We ignore the possibility of namespaces since user-generated HTML + // can't use them anymore. + return (bool)preg_match( '/^data-(ooui|mw|parsoid)/i', $attr ); + } + /** * Merge two sets of HTML attributes. Conflicting items in the second set * will override those in the first, except for 'class' attributes which diff --git a/includes/actions/HistoryAction.php b/includes/actions/HistoryAction.php index d1be7d4b1ba..7460340a964 100644 --- a/includes/actions/HistoryAction.php +++ b/includes/actions/HistoryAction.php @@ -780,9 +780,11 @@ class HistoryPager extends ReverseChronologicalPager { $s .= ' . . ' . $s2; } - Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes ] ); + $attribs = [ 'data-mw-revid' => $rev->getId() ]; + + Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - $attribs = []; if ( $classes ) { $attribs['class'] = implode( ' ', $classes ); } diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index 92a3d3f2e22..00d842f4cb0 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -739,4 +739,26 @@ class ChangesList extends ContextSource { && intval( $rcObj->getAttribute( 'rc_this_oldid' ) ) === 0; } + /** + * Get recommended data attributes for a change line. + * @param RecentChange $rc + * @return string[] attribute name => value + */ + protected function getDataAttributes( RecentChange $rc ) { + $type = $rc->getAttribute( 'rc_source' ); + switch ( $type ) { + case RecentChange::SRC_EDIT: + case RecentChange::SRC_NEW: + return [ + 'data-mw-revid' => $rc->mAttribs['rc_this_oldid'], + ]; + case RecentChange::SRC_LOG: + return [ + 'data-mw-logid' => $rc->mAttribs['rc_logid'], + 'data-mw-logaction' => $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'], + ]; + default: + return []; + } + } } diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index b34a33fdcfe..03f63f673f5 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -447,13 +447,16 @@ class EnhancedChangesList extends ChangesList { # Tags $data['tags'] = $this->getTags( $rcObj, $classes ); + $attribs = $this->getDataAttributes( $rcObj ); + // give the hook a chance to modify the data $success = Hooks::run( 'EnhancedChangesListModifyLineData', - [ $this, &$data, $block, $rcObj, &$classes ] ); + [ $this, &$data, $block, $rcObj, &$classes, &$attribs ] ); if ( !$success ) { // skip entry if hook aborted it return []; } + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); $lineParams['recentChangesFlagsRaw'] = []; if ( isset( $data['recentChangesFlags'] ) ) { @@ -469,6 +472,7 @@ class EnhancedChangesList extends ChangesList { } $lineParams['classes'] = array_values( $classes ); + $lineParams['attribs'] = Html::expandAttributes( $attribs ); // everything else: makes it easier for extensions to add or remove data $lineParams['data'] = array_values( $data ); @@ -671,6 +675,8 @@ class EnhancedChangesList extends ChangesList { # Show how many people are watching this if enabled $data['watchingUsers'] = $this->numberofWatchingusers( $rcObj->numberofWatchingusers ); + $data['attribs'] = array_merge( $this->getDataAttributes( $rcObj ), [ 'class' => $classes ] ); + // give the hook a chance to modify the data $success = Hooks::run( 'EnhancedChangesListModifyBlockLineData', [ $this, &$data, $rcObj ] ); @@ -678,9 +684,11 @@ class EnhancedChangesList extends ChangesList { // skip entry if hook aborted it return ''; } + $attribs = $data['attribs']; + unset( $data['attribs'] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - $line = Html::openElement( 'table', [ 'class' => $classes ] ) . - Html::openElement( 'tr' ); + $line = Html::openElement( 'table', $attribs ) . Html::openElement( 'tr' ); $line .= ''; if ( isset( $data['recentChangesFlags'] ) ) { diff --git a/includes/changes/OldChangesList.php b/includes/changes/OldChangesList.php index a5d5191da81..2a53d6694df 100644 --- a/includes/changes/OldChangesList.php +++ b/includes/changes/OldChangesList.php @@ -50,16 +50,23 @@ class OldChangesList extends ChangesList { $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] ); } + $attribs = $this->getDataAttributes( $rc ); + // Avoid PHP 7.1 warning from passing $this by reference $list = $this; - if ( !Hooks::run( 'OldChangesListRecentChangesLine', [ &$list, &$html, $rc, &$classes ] ) ) { + if ( !Hooks::run( 'OldChangesListRecentChangesLine', + [ &$list, &$html, $rc, &$classes, &$attribs ] ) + ) { return false; } + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); $dateheader = ''; // $html now contains only
  • ...
  • , for hooks' convenience. $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); - return "$dateheader
  • " . $html . "
  • \n"; + $attribs['class'] = implode( ' ', $classes ); + + return $dateheader . Html::rawElement( 'li', $attribs, $html ) . "\n"; } /** diff --git a/includes/logging/LogEventsList.php b/includes/logging/LogEventsList.php index 317652a3b7a..c5501cbf9bc 100644 --- a/includes/logging/LogEventsList.php +++ b/includes/logging/LogEventsList.php @@ -390,9 +390,18 @@ class LogEventsList extends ContextSource { [ 'mw-logline-' . $entry->getType() ], $newClasses ); + $attribs = [ + 'data-mw-logid' => $entry->getId(), + 'data-mw-logaction' => $entry->getFullType(), + ]; + $ret = "$del $time $action $comment $revert $tagDisplay"; - return Html::rawElement( 'li', [ 'class' => $classes ], - "$del $time $action $comment $revert $tagDisplay" ) . "\n"; + // Let extensions add data + Hooks::run( 'LogEventsListLineEnding', [ $this, &$ret, $entry, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); + $attribs['class'] = implode( ' ', $classes ); + + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php index be8ad8fb40f..83482f6f2fb 100644 --- a/includes/specials/SpecialNewpages.php +++ b/includes/specials/SpecialNewpages.php @@ -314,6 +314,7 @@ class SpecialNewpages extends IncludableSpecialPage { $rev->setTitle( $title ); $classes = []; + $attribs = [ 'data-mw-revid' => $result->rev_id ]; $lang = $this->getLanguage(); $dm = $lang->getDirMark(); @@ -378,11 +379,19 @@ class SpecialNewpages extends IncludableSpecialPage { $tagDisplay = ''; } - $css = count( $classes ) ? ' class="' . implode( ' ', $classes ) . '"' : ''; - # Display the old title if the namespace/title has been changed $oldTitleText = ''; $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title ); + $ret = "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} " + . "{$tagDisplay} {$oldTitleText}"; + + // Let extensions add data + Hooks::run( 'NewPagesLineEnding', [ $this, &$ret, $result, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); + + if ( count( $classes ) ) { + $attribs['class'] = implode( ' ', $classes ); + } if ( !$title->equals( $oldTitle ) ) { $oldTitleText = $oldTitle->getPrefixedText(); @@ -393,8 +402,7 @@ class SpecialNewpages extends IncludableSpecialPage { ); } - return "{$time} {$dm}{$plink} {$hist} {$dm}{$length} " - . "{$dm}{$ulink} {$comment} {$tagDisplay} {$oldTitleText}\n"; + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/specials/pagers/ContribsPager.php b/includes/specials/pagers/ContribsPager.php index a3880eed509..6bd7eb0e9fd 100644 --- a/includes/specials/pagers/ContribsPager.php +++ b/includes/specials/pagers/ContribsPager.php @@ -365,9 +365,9 @@ class ContribsPager extends RangeChronologicalPager { * @return string */ function formatRow( $row ) { - $ret = ''; $classes = []; + $attribs = []; $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); @@ -388,7 +388,7 @@ class ContribsPager extends RangeChronologicalPager { MediaWiki\restoreWarnings(); if ( $validRevision ) { - $classes = []; + $attribs['data-mw-revid'] = $rev->getId(); $page = Title::newFromRow( $row ); $link = $linkRenderer->makeLink( @@ -535,19 +535,21 @@ class ContribsPager extends RangeChronologicalPager { } // Let extensions add data - Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + Hooks::run( 'ContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); // TODO: Handle exceptions in the catch block above. Do any extensions rely on // receiving empty rows? - if ( $classes === [] && $ret === '' ) { + if ( $classes === [] && $attribs === [] && $ret === '' ) { wfDebug( "Dropping Special:Contribution row that could not be formatted\n" ); return "\n"; } + $attribs['class'] = $classes; // FIXME: The signature of the ContributionsLineEnding hook makes it // very awkward to move this LI wrapper into the template. - return Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + return Html::rawElement( 'li', $attribs, $ret ) . "\n"; } /** diff --git a/includes/specials/pagers/DeletedContribsPager.php b/includes/specials/pagers/DeletedContribsPager.php index 78e1092dc5e..43d7ad40c7b 100644 --- a/includes/specials/pagers/DeletedContribsPager.php +++ b/includes/specials/pagers/DeletedContribsPager.php @@ -195,6 +195,7 @@ class DeletedContribsPager extends IndexPager { function formatRow( $row ) { $ret = ''; $classes = []; + $attribs = []; /* * There may be more than just revision rows. To make sure that we'll only be processing @@ -213,17 +214,20 @@ class DeletedContribsPager extends IndexPager { MediaWiki\restoreWarnings(); if ( $validRevision ) { + $attribs['data-mw-revid'] = $rev->getId(); $ret = $this->formatRevisionRow( $row ); } // Let extensions add data - Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes ] ); + Hooks::run( 'DeletedContributionsLineEnding', [ $this, &$ret, $row, &$classes, &$attribs ] ); + $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] ); - if ( $classes === [] && $ret === '' ) { + if ( $classes === [] && $attribs === [] && $ret === '' ) { wfDebug( "Dropping Special:DeletedContribution row that could not be formatted\n" ); $ret = "\n"; } else { - $ret = Html::rawElement( 'li', [ 'class' => $classes ], $ret ) . "\n"; + $attribs['class'] = $classes; + $ret = Html::rawElement( 'li', $attribs, $ret ) . "\n"; } return $ret; diff --git a/includes/templates/EnhancedChangesListGroup.mustache b/includes/templates/EnhancedChangesListGroup.mustache index 352eb17d21e..3a37c2ebcc2 100644 --- a/includes/templates/EnhancedChangesListGroup.mustache +++ b/includes/templates/EnhancedChangesListGroup.mustache @@ -14,7 +14,7 @@ {{# lines }} - + {{{ recentChangesFlags }}}  diff --git a/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php b/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php new file mode 100644 index 00000000000..afd80ff7a28 --- /dev/null +++ b/tests/phpunit/includes/GlobalFunctions/wfArrayFilterTest.php @@ -0,0 +1,37 @@ + 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $key !== 'b'; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $val !== 2; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a', 'b', 'c' ]; + $filtered = wfArrayFilter( $arr, function( $val, $key ) { + return $key !== 0; + } ); + $this->assertSame( [ 1 => 'b', 2 => 'c' ], $filtered ); + } + + public function testWfArrayFilterByKey() { + $arr = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + $filtered = wfArrayFilterByKey( $arr, function( $key ) { + return $key !== 'b'; + } ); + $this->assertSame( [ 'a' => 1, 'c' => 3 ], $filtered ); + + $arr = [ 'a', 'b', 'c' ]; + $filtered = wfArrayFilterByKey( $arr, function( $key ) { + return $key !== 0; + } ); + $this->assertSame( [ 1 => 'b', 2 => 'c' ], $filtered ); + } +} diff --git a/tests/phpunit/includes/SanitizerTest.php b/tests/phpunit/includes/SanitizerTest.php index 862b7d07d50..c237c509a77 100644 --- a/tests/phpunit/includes/SanitizerTest.php +++ b/tests/phpunit/includes/SanitizerTest.php @@ -362,4 +362,25 @@ class SanitizerTest extends MediaWikiTestCase { [ '+1 +2', '+1', '+2' ], ]; } + + /** + * @dataProvider provideIsReservedDataAttribute + */ + public function testIsReservedDataAttribute( $attr, $expected ) { + $this->assertSame( $expected, Sanitizer::isReservedDataAttribute( $attr ) ); + } + + public static function provideIsReservedDataAttribute() { + return [ + [ 'foo', false ], + [ 'data', false ], + [ 'data-foo', false ], + [ 'data-mw', true ], + [ 'data-ooui', true ], + [ 'data-parsoid', true ], + [ 'data-mw-foo', true ], + [ 'data-ooui-foo', true ], + [ 'data-mwfoo', true ], // could be false but this is how it's implemented currently + ]; + } } diff --git a/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/tests/phpunit/includes/changes/EnhancedChangesListTest.php index 308e6de11e8..029d1fe3863 100644 --- a/tests/phpunit/includes/changes/EnhancedChangesListTest.php +++ b/tests/phpunit/includes/changes/EnhancedChangesListTest.php @@ -98,6 +98,9 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { $recentChange = $this->getEditChange( '20131103092153' ); $enhancedChangesList->recentChangesLine( $recentChange, false ); + $html = $enhancedChangesList->endRecentChangesList(); + $this->assertContains( 'data-mw-revid="5"', $html ); + $recentChange2 = $this->getEditChange( '20131103092253' ); $enhancedChangesList->recentChangesLine( $recentChange2, false ); @@ -105,6 +108,13 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches ); $this->assertCount( 2, $matches[0] ); + + $recentChange3 = $this->getLogChange(); + $enhancedChangesList->recentChangesLine( $recentChange3, false ); + + $html = $enhancedChangesList->endRecentChangesList(); + $this->assertContains( 'data-mw-logaction="foo/bar"', $html ); + $this->assertContains( 'data-mw-logid="25"', $html ); } /** @@ -129,6 +139,15 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase { return $recentChange; } + private function getLogChange() { + $user = $this->getMutableTestUser()->getUser(); + $recentChange = $this->testRecentChangesHelper->makeLogRecentChange( 'foo', 'bar', $user, + 'Title', '20131103092153', 0, 0 + ); + + return $recentChange; + } + /** * @return RecentChange */ diff --git a/tests/phpunit/includes/changes/OldChangesListTest.php b/tests/phpunit/includes/changes/OldChangesListTest.php index 51cfadcb6f6..f892eb70ed8 100644 --- a/tests/phpunit/includes/changes/OldChangesListTest.php +++ b/tests/phpunit/includes/changes/OldChangesListTest.php @@ -119,15 +119,17 @@ class OldChangesListTest extends MediaWikiLangTestCase { ); } - public function testRecentChangesLine_Tags() { + public function testRecentChangesLine_Attribs() { $recentChange = $this->getEditChange(); $recentChange->mAttribs['ts_tags'] = 'vandalism,newbie'; $oldChangesList = $this->getOldChangesList(); $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 ); - $this->assertRegExp( '/
  • /', $line ); - $this->assertRegExp( '/
  • /', $line ); + $this->assertRegExp( '/
  • /', + $line ); + $this->assertRegExp( '/
  • /', + $line ); } public function testRecentChangesLine_numberOfWatchingUsers() {