"mw-changeslist-line-watched" or "mw-changeslist-line-not-watched", and the
title itself is surrounded by <span></span> tags with class "mw-title".
* Added ContribsPager::reallyDoQuery hook allowing extensions to data to MyContribs
+* Added new hook ParserAfterParse to allow extensions to affect parsed output
+ after the parse is complete but before block level processing, link holder
+ replacement, and so on.
+* (bug 34678) Added InternalParseBeforeSanitize hook which gets called during Parser's
+ internalParse method just before the parser removes unwanted/dangerous HTML tags.
=== Bug fixes in 1.20 ===
* (bug 30245) Use the correct way to construct a log page title.
* (bug 31895) mw.loader mode now correct when triggered from a $.fn.ready
handler that is bound before mediawiki.js's handler (e.g. browser-userscripts
like greasemonkey).
+* (bug 38152) jquery.tablesorter: Use .data() instead of .attr(), so that live
+ values are used instead of just the fixed values from when the tablesorter
+ was initialized.
=== API changes in 1.20 ===
* (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
* The paraminfo module now also contains result properties for most modules
* (bug 32348) Allow descending order for list=alllinks
* (bug 31777) Upload unknown error ``fileexists-forbidden''
+* (bug 32382) Allow descending order for list=iwbacklinks
+* (bug 32381) Allow descending order for list=backlinks, list=embeddedin and list=imageusage
+* (bug 32383) Allow descending order for list=langbacklinks
+* API meta=siteinfo can now return the list of known variable IDs
=== Languages updated in 1.20 ===
&$iwData: output array describing the interwiki with keys iw_url, iw_local,
iw_trans and optionally iw_api and iw_wikiid.
+'InternalParseBeforeSanitize': during Parser's internalParse method just before the
+parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/
+onlyinclude and other processings. Ideal for syntax-extensions after template/parser
+function execution which respect nowiki and HTML-comments.
+&$parser: Parser object
+&$text: string containing partially parsed text
+&$stripState: Parser's internal StripState object
+
'InternalParseBeforeLinks': during Parser's internalParse method before links
-but after noinclude/includeonly/onlyinclude and other processing.
+but after nowiki/noinclude/includeonly/onlyinclude and other processings.
&$parser: Parser object
&$text: string containing partially parsed text
&$stripState: Parser's internal StripState object
this hook and append its values to the key.
$hash: reference to a hash key string which can be modified
+'ParserAfterParse': Called from Parser::parse() just after the call to
+Parser::internalParse() returns
+$parser: parser object
+$text: text being parsed
+$stripState: stripState used (object)
+
'ParserAfterStrip': Same as ParserBeforeStrip
'ParserAfterTidy': Called after Parser::tidy() in Parser::parse()
private $rootTitle;
private $params, $contID, $redirID, $redirect;
- private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS;
+ private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS;
/**
* Maps ns and title to pageid
$this->hasNS = $moduleName !== 'imageusage';
if ( $this->hasNS ) {
$this->bl_title = $prefix . '_title';
- $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}";
$this->bl_fields = array(
$this->bl_ns,
$this->bl_title
);
} else {
$this->bl_title = $prefix . '_to';
- $this->bl_sort = "{$this->bl_title}, {$this->bl_from}";
$this->bl_fields = array(
$this->bl_title
);
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
if ( !is_null( $this->contID ) ) {
- $this->addWhere( "{$this->bl_from}>={$this->contID}" );
+ $op = $this->params['dir'] == 'descending' ? '<' : '>';
+ $this->addWhere( "{$this->bl_from}$op={$this->contID}" );
}
if ( $this->params['filterredir'] == 'redirects' ) {
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
- $this->addOption( 'ORDER BY', $this->bl_from );
+ $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
+ $this->addOption( 'ORDER BY', $this->bl_from . $sort );
$this->addOption( 'STRAIGHT_JOIN' );
}
// We can't use LinkBatch here because $this->hasNS may be false
$titleWhere = array();
+ $allRedirNs = array();
+ $allRedirDBkey = array();
foreach ( $this->redirTitles as $t ) {
- $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $t->getDBkey() ) .
- ( $this->hasNS ? " AND {$this->bl_ns} = {$t->getNamespace()}" : '' );
+ $redirNs = $t->getNamespace();
+ $redirDBkey = $t->getDBkey();
+ $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) .
+ ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' );
+ $allRedirNs[] = $redirNs;
+ $allRedirDBkey[] = $redirDBkey;
}
$this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
$this->addWhereFld( 'page_namespace', $this->params['namespace'] );
if ( !is_null( $this->redirID ) ) {
+ $op = $this->params['dir'] == 'descending' ? '<' : '>';
$first = $this->redirTitles[0];
$title = $db->addQuotes( $first->getDBkey() );
$ns = $first->getNamespace();
$from = $this->redirID;
if ( $this->hasNS ) {
- $this->addWhere( "{$this->bl_ns} > $ns OR " .
+ $this->addWhere( "{$this->bl_ns} $op $ns OR " .
"({$this->bl_ns} = $ns AND " .
- "({$this->bl_title} > $title OR " .
+ "({$this->bl_title} $op $title OR " .
"({$this->bl_title} = $title AND " .
- "{$this->bl_from} >= $from)))" );
+ "{$this->bl_from} $op= $from)))" );
} else {
- $this->addWhere( "{$this->bl_title} > $title OR " .
+ $this->addWhere( "{$this->bl_title} $op $title OR " .
"({$this->bl_title} = $title AND " .
- "{$this->bl_from} >= $from)" );
+ "{$this->bl_from} $op= $from)" );
}
}
if ( $this->params['filterredir'] == 'redirects' ) {
}
$this->addOption( 'LIMIT', $this->params['limit'] + 1 );
- $this->addOption( 'ORDER BY', $this->bl_sort );
+ $orderBy = array();
+ $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' );
+ // Don't order by namespace/title if it's constant in the WHERE clause
+ if( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) {
+ $orderBy[] = $this->bl_ns . $sort;
+ }
+ if( count( array_unique( $allRedirDBkey ) ) != 1 ) {
+ $orderBy[] = $this->bl_title . $sort;
+ }
+ $orderBy[] = $this->bl_from . $sort;
+ $this->addOption( 'ORDER BY', $orderBy );
$this->addOption( 'USE INDEX', array( 'page' => 'PRIMARY' ) );
}
ApiBase::PARAM_ISMULTI => true,
ApiBase::PARAM_TYPE => 'namespace'
),
+ 'dir' => array(\r
+ ApiBase::PARAM_DFLT => 'ascending',\r
+ ApiBase::PARAM_TYPE => array(\r
+ 'ascending',\r
+ 'descending'\r
+ )\r
+ ),
'filterredir' => array(
ApiBase::PARAM_DFLT => 'all',
ApiBase::PARAM_TYPE => array(
'pageid' => "Pageid to search. Cannot be used together with {$this->bl_code}title",
'continue' => 'When more results are available, use this to continue',
'namespace' => 'The namespace to enumerate',
+ 'dir' => 'The direction in which to list',
);
if ( $this->getModuleName() != 'embeddedin' ) {
return array_merge( $retval, array(
}
$db = $this->getDB();
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$prefix = $db->addQuotes( $cont[0] );
$title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
$from = intval( $cont[2] );
$this->addWhere(
- "iwl_prefix > $prefix OR " .
+ "iwl_prefix $op $prefix OR " .
"(iwl_prefix = $prefix AND " .
- "(iwl_title > $title OR " .
+ "(iwl_title $op $title OR " .
"(iwl_title = $title AND " .
- "iwl_from >= $from)))"
+ "iwl_from $op= $from)))"
);
}
$this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect',
'iwl_from', 'iwl_prefix', 'iwl_title' ) );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
if ( isset( $params['prefix'] ) ) {
$this->addWhereFld( 'iwl_prefix', $params['prefix'] );
if ( isset( $params['title'] ) ) {
$this->addWhereFld( 'iwl_title', $params['title'] );
- $this->addOption( 'ORDER BY', 'iwl_from' );
+ $this->addOption( 'ORDER BY', 'iwl_from' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'iwl_title',
- 'iwl_from'
+ 'iwl_title' . $sort,
+ 'iwl_from' . $sort
));
}
} else {
$this->addOption( 'ORDER BY', array(
- 'iwl_prefix',
- 'iwl_title',
- 'iwl_from'
+ 'iwl_prefix' . $sort,
+ 'iwl_title' . $sort,
+ 'iwl_from' . $sort
));
}
'iwtitle',
),
),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ),
);
}
' iwtitle - Adds the title of the interwiki',
),
'limit' => 'How many total pages to return',
+ 'dir' => 'The direction in which to list',
);
}
}
$db = $this->getDB();
+ $op = $params['dir'] == 'descending' ? '<' : '>';
$prefix = $db->addQuotes( $cont[0] );
$title = $db->addQuotes( $this->titleToKey( $cont[1] ) );
$from = intval( $cont[2] );
$this->addWhere(
- "ll_lang > $prefix OR " .
+ "ll_lang $op $prefix OR " .
"(ll_lang = $prefix AND " .
- "(ll_title > $title OR " .
+ "(ll_title $op $title OR " .
"(ll_title = $title AND " .
- "ll_from >= $from)))"
+ "ll_from $op= $from)))"
);
}
$this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect',
'll_from', 'll_lang', 'll_title' ) );
+ $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' );
if ( isset( $params['lang'] ) ) {
$this->addWhereFld( 'll_lang', $params['lang'] );
if ( isset( $params['title'] ) ) {
$this->addWhereFld( 'll_title', $params['title'] );
- $this->addOption( 'ORDER BY', 'll_from' );
+ $this->addOption( 'ORDER BY', 'll_from' . $sort );
} else {
$this->addOption( 'ORDER BY', array(
- 'll_title',
- 'll_from'
+ 'll_title' . $sort,
+ 'll_from' . $sort
));
}
} else {
$this->addOption( 'ORDER BY', array(
- 'll_lang',
- 'll_title',
- 'll_from'
+ 'll_lang' . $sort,
+ 'll_title' . $sort,
+ 'll_from' . $sort
));
}
'lltitle',
),
),
+ 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ),
);
}
' lltitle - Adds the title of the language ink',
),
'limit' => 'How many total pages to return',
+ 'dir' => 'The direction in which to list',
);
}
case 'showhooks':
$fit = $this->appendSubscribedHooks( $p );
break;
+ case 'variables':
+ $fit = $this->appendVariables( $p );
+ break;
default:
ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" );
}
return $this->getResult()->addValue( 'query', $property, $hooks );
}
+ public function appendVariables( $property ) {
+ $variables = MagicWord::getVariableIDs();
+ $this->getResult()->setIndexedTagName( $variables, 'v' );
+ return $this->getResult()->addValue( 'query', $property, $variables );
+ }
+
private function formatParserTags( $item ) {
return "<{$item}>";
}
'extensiontags',
'functionhooks',
'showhooks',
+ 'variables',
)
),
'filteriw' => array(
' skins - Returns a list of all enabled skins',
' extensiontags - Returns a list of parser extension tags',
' functionhooks - Returns a list of parser function hooks',
- ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)'
+ ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)',
+ ' variables - Returns a list of variable IDs',
),
'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
'showalldb' => 'List all database servers, not just the one lagging the most',
$deviceName = 'android';
if ( strpos( $userAgent, 'Opera Mini' ) !== false ) {
$deviceName = 'operamini';
+ } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
+ $deviceName = 'operamobile';
}
- } else if ( preg_match( '/MSIE 9.0/', $userAgent ) ||
+ } elseif ( preg_match( '/MSIE 9.0/', $userAgent ) ||
preg_match( '/MSIE 8.0/', $userAgent ) ) {
$deviceName = 'ie';
- } else if( preg_match( '/MSIE/', $userAgent ) ) {
+ } elseif( preg_match( '/MSIE/', $userAgent ) ) {
$deviceName = 'html';
- } else if ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
+ } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
$deviceName = 'operamobile';
} elseif ( preg_match( '/iPad.* Safari/', $userAgent ) ) {
$deviceName = 'iphone';
$deviceName = 'wii';
} elseif ( strpos( $userAgent, 'Opera Mini' ) !== false ) {
$deviceName = 'operamini';
- } elseif ( strpos( $userAgent, 'Opera Mobi' ) !== false ) {
- $deviceName = 'iphone';
} else {
- $deviceName = 'webkit';
+ $deviceName = 'operamobile';
}
} elseif ( preg_match( '/Kindle\/1.0/', $userAgent ) ) {
$deviceName = 'kindle';
# No more strip!
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
$text = $this->internalParse( $text );
+ wfRunHooks( 'ParserAfterParse', array( &$this, &$text, &$this->mStripState ) );
$text = $this->mStripState->unstripGeneral( $text );
$text = $this->replaceVariables( $text );
}
+ wfRunHooks( 'InternalParseBeforeSanitize', array( &$this, &$text, &$this->mStripState ) );
$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
--- /dev/null
+<?php\r
+/**\r
+ * Description: This script takes $wgHiddenPrefs and removes their preference from the DB. [[bugzilla:30976]]\r
+ * @author TyA <tya.wiki@gmail.com>\r
+ * @ingroup Maintenance\r
+ */\r
+\r
+require_once( dirname( __FILE__ ) . '/Maintenance.php' );\r
+\r
+class CleanupPreferences extends Maintenance {\r
+ public function execute() {\r
+ global $wgHiddenPrefs;\r
+\r
+ $dbw = wfGetDB( DB_MASTER );\r
+ $dbw->begin();\r
+ foreach( $wgHiddenPrefs as $item ) {\r
+ $dbw->delete(\r
+ 'user_properties',\r
+ array( 'up_property' => $item ),\r
+ __METHOD__\r
+ );\r
+ };\r
+ $dbw->commit();\r
+ $this->output( "Finished!\n" );\r
+ }\r
+}\r
+\r
+$maintClass = 'CleanupPreferences'; // Tells it to run the class\r
+require_once( RUN_MAINTENANCE_IF_MAIN );\r
/* Local scope */
- var ts,
+ var ts,
parsers = [];
/* Parser utility functions */
function getElementText( node ) {
var $node = $( node ),
- data = $node.attr( 'data-sort-value' );
- if ( data !== undefined ) {
- return data;
+ // Use data-sort-value attribute.
+ // Use data() instead of attr() so that live value changes
+ // are processed as well (bug 38152).
+ data = $node.data( 'sortValue' );
+
+ if ( data !== null && data !== undefined ) {
+ // Cast any numbers or other stuff to a string, methods
+ // like charAt, toLowerCase and split are expected.
+ return String( data );
} else {
return $node.text();
}
explodeRowspans( $table );
// try to auto detect column type, and store in tables config
table.config.parsers = buildParserCache( table, $headers );
- // build the cache for the tbody cells
- cache = buildCache( table );
}
+
+ // Build the cache for the tbody cells
+ // to share between calculations for this sort action.
+ // Re-calculated each time a sort action is performed due to possiblity
+ // that sort values change. Shouldn't be too expensive, but if it becomes
+ // too slow an event based system should be implemented somehow where
+ // cells get event .change() and bubbles up to the <table> here
+ cache = buildCache( table );
+
var totalRows = ( $table[0].tBodies[0] && $table[0].tBodies[0].rows.length ) || 0;
if ( !table.sortDisabled && totalRows > 0 ) {
* Generates a random user session ID (32 alpha-numeric characters).
*
* This information would potentially be stored in a cookie to identify a user during a
- * session or series of sessions. It's uniqueness should not be depended on.
+ * session or series of sessions. Its uniqueness should not be depended on.
*
* @return String: Random set of 32 alpha-numeric characters
*/
// This is kind of ugly but we're stuck with this for b/c reasons
mw.user = new User( mw.user.options, mw.user.tokens );
-})(jQuery);
\ No newline at end of file
+})(jQuery);
test( 'data-sort-value attribute, when available, should override sorting position', function() {
var $table, data;
- // Simple example, one without data-sort-value which should be sorted at it's text.
+ // Example 1: All cells except one cell without data-sort-value,
+ // which should be sorted at it's text content value.
$table = $(
'<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
'<tbody>' +
data = [];
$table.find( 'tbody > tr' ).each( function( i, tr ) {
$( tr ).find( 'td' ).each( function( i, td ) {
- data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } );
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
});
});
deepEqual( data, [
{
- "data": "Apple",
- "text": "Bird"
+ data: 'Apple',
+ text: 'Bird'
}, {
- "data": "Bananna",
- "text": "Ferret"
+ data: 'Bananna',
+ text: 'Ferret'
}, {
- "data": undefined,
- "text": "Cheetah"
+ data: undefined,
+ text: 'Cheetah'
}, {
- "data": "Cherry",
- "text": "Dolphin"
+ data: 'Cherry',
+ text: 'Dolphin'
}, {
- "data": "Drupe",
- "text": "Elephant"
+ data: 'Drupe',
+ text: 'Elephant'
}
- ] );
+ ], 'Order matches expected order (based on data-sort-value attribute values)' );
- // Another example
+ // Example 2
$table = $(
'<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
'<tbody>' +
data = [];
$table.find( 'tbody > tr' ).each( function( i, tr ) {
$( tr ).find( 'td' ).each( function( i, td ) {
- data.push( { data: $( td ).data( 'sort-value' ), text: $( td ).text() } );
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
});
});
deepEqual( data, [
{
- "data": undefined,
- "text": "B"
+ data: undefined,
+ text: 'B'
}, {
- "data": undefined,
- "text": "D"
+ data: undefined,
+ text: 'D'
}, {
- "data": "E",
- "text": "A"
+ data: 'E',
+ text: 'A'
}, {
- "data": "F",
- "text": "C"
+ data: 'F',
+ text: 'C'
}, {
- "data": undefined,
- "text": "G"
+ data: undefined,
+ text: 'G'
}
- ] );
+ ], 'Order matches expected order (based on data-sort-value attribute values)' );
+
+ // Example 3: Test that live changes are used from data-sort-value,
+ // even if they change after the tablesorter is constructed (bug 38152).
+ $table = $(
+ '<table class="sortable"><thead><tr><th>Data</th></tr></thead>' +
+ '<tbody>' +
+ '<tr><td>D</td></tr>' +
+ '<tr><td data-sort-value="1">A</td></tr>' +
+ '<tr><td>B</td></tr>' +
+ '<tr><td data-sort-value="2">G</td></tr>' +
+ '<tr><td>C</td></tr>' +
+ '</tbody></table>'
+ );
+ // initialize table sorter and sort once
+ $table
+ .tablesorter()
+ .find( '.headerSort:eq(0)' ).click();
+
+ // Change the sortValue data properties (bug 38152)
+ // - change data
+ $table.find( 'td:contains(A)' ).data( 'sortValue', 3 );
+ // - add data
+ $table.find( 'td:contains(B)' ).data( 'sortValue', 1 );
+ // - remove data, bring back attribute: 2
+ $table.find( 'td:contains(G)' ).removeData( 'sortValue' );
+
+ // Now sort again (twice, so it is back at Ascending)
+ $table.find( '.headerSort:eq(0)' ).click();
+ $table.find( '.headerSort:eq(0)' ).click();
+
+ data = [];
+ $table.find( 'tbody > tr' ).each( function( i, tr ) {
+ $( tr ).find( 'td' ).each( function( i, td ) {
+ data.push( {
+ data: $( td ).data( 'sortValue' ),
+ text: $( td ).text()
+ } );
+ });
+ });
+
+ deepEqual( data, [
+ {
+ data: 1,
+ text: "B"
+ }, {
+ data: 2,
+ text: "G"
+ }, {
+ data: 3,
+ text: "A"
+ }, {
+ data: undefined,
+ text: "C"
+ }, {
+ data: undefined,
+ text: "D"
+ }
+ ], 'Order matches expected order, using the current sortValue in $.data()' );
});
var $table;
$table = $(
- '<table class="sortable" id="32888">' +
- '<tr><th>header<table id="32888-2">'+
+ '<table class="sortable" id="mw-bug-32888">' +
+ '<tr><th>header<table id="mw-bug-32888-2">'+
'<tr><th>1</th><th>2</th></tr>' +
'</table></th></tr>' +
'<tr><td>A</td></tr>' +
'Child tables inside a headercell should not interfere with sortable headers (bug 32888)'
);
equals(
- $('#32888-2').find('th.headerSort').length,
+ $( '#mw-bug-32888-2' ).find('th.headerSort').length,
0,
'The headers of child tables inside a headercell should not be sortable themselves (bug 32888)'
);
});
+
var correctDateSorting1 = [
['01 January 2010'],
['05 February 2010'],