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 @@
|