if ( session_id() !== $session->getId() ) {
session_id( $session->getId() );
}
-
+ MediaWiki\quietCall( 'session_cache_limiter', 'private, must-revalidate' );
MediaWiki\quietCall( 'session_start' );
}
array(
'page_id' => $pageId,
'page_latest=rev_id',
- ),
- __METHOD__ );
+ ),
+ __METHOD__,
+ array( 'FOR UPDATE' ) // T51581
+ );
if ( $current ) {
if ( !$user ) {
) {
// Start the PHP-session for backwards compatibility
session_id( $session->getId() );
+ MediaWiki\quietCall( 'session_cache_limiter', 'private, must-revalidate' );
MediaWiki\quietCall( 'session_start' );
}
}
);
// Per RFC 6265, key is name + domain + path
- $key = "{$data['name']}\n{$data['domain']}\n{$date['path']}";
+ $key = "{$data['name']}\n{$data['domain']}\n{$data['path']}";
// If this cookie name was in the request, fake an entry in
// self::$setCookies for it so the deleting check works right.
$wgUser = $context->getUser(); // b/c
if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
session_id( $session->getId() );
+ MediaWiki\quietCall( 'session_cache_limiter', 'private, must-revalidate' );
MediaWiki\quietCall( 'session_start' );
}
$request = new FauxRequest( array(), false, $session );
// Add the log entry...
$logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
+ $logEntry->setTimestamp( $this->timestamp );
$logEntry->setPerformer( $user );
$logEntry->setComment( $comment );
$logEntry->setTarget( $descTitle );
$forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
if ( $forceHTTPS ) {
- $options['secure'] = true;
+ // Don't set the secure flag if the request came in
+ // over "http", for backwards compat.
+ // @todo Break that backwards compat properly.
+ $options['secure'] = $this->config->get( 'CookieSecure' );
}
$response->setCookie( $this->params['sessionName'], $session->getId(), null,
) {
$this->logger->debug( "SessionBackend $this->id: Taking over PHP session" );
session_id( (string)$this->id );
+ \MediaWiki\quietCall( 'session_cache_limiter', 'private, must-revalidate' );
\MediaWiki\quietCall( 'session_start' );
}
}
$this->setTopText( $opts );
$lang = $this->getLanguage();
- $wlInfo = '';
if ( $opts['days'] > 0 ) {
- $timestamp = wfTimestampNow();
- $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $opts['days'] * 24 ) )->params(
- $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
- )->parse() . "<br />\n";
+ $days = $opts['days'];
+ } else {
+ $days = $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 );
}
+ $timestamp = wfTimestampNow();
+ $wlInfo = $this->msg( 'wlnote' )->numParams( $numRows, round( $days * 24 ) )->params(
+ $lang->userDate( $timestamp, $user ), $lang->userTime( $timestamp, $user )
+ )->parse() . "<br />\n";
$nondefaults = $opts->getChangedValues();
$cutofflinks = $this->msg( 'wlshowtime' ) . ' ' . $this->cutoffselector( $opts );
$days[] = $userWatchlistOption;
}
+ $maxDays = (string)( $this->getConfig()->get( 'RCMaxAge' ) / ( 3600 * 24 ) );
+ // add the maximum possible value, if it isn't available already
+ if ( !in_array( $maxDays, $days ) ) {
+ $days[] = $maxDays;
+ }
+
$selected = (string)$options['days'];
+ if ( $selected <= 0 ) {
+ $selected = $maxDays;
+ }
+
// add the currently selected value, if it isn't available already
- if ( !in_array( $selected, $days ) && $selected !== '0' ) {
+ if ( !in_array( $selected, $days ) ) {
$days[] = $selected;
}
$select->addOption( $name, $value );
}
- // 'all' option
- $name = $this->msg( 'watchlistall2' )->text();
- $value = '0';
- $select->addOption( $name, $value );
-
return $select->getHTML() . "\n<br />\n";
}
"wlheader-showupdated": "Pages that have been changed since you last visited them are shown in <strong>bold</strong>.",
"wlnote": "Below {{PLURAL:$1|is the last change|are the last <strong>$1</strong> changes}} in the last {{PLURAL:$2|hour|<strong>$2</strong> hours}}, as of $3, $4.",
"wlshowlast": "Show last $1 hours $2 days",
- "watchlistall2": "all",
"watchlist-hide": "Hide",
"watchlist-submit": "Show",
"wlshowtime": "Period of time to display:",
"expand_templates_preview_fail_html": "<em>Because {{SITENAME}} has raw HTML enabled and there was a loss of session data, the preview is hidden as a precaution against JavaScript attacks.</em>\n\n<strong>If this is a legitimate preview attempt, please try again.</strong>\nIf it still does not work, try [[Special:UserLogout|logging out]] and logging back in.",
"expand_templates_preview_fail_html_anon": "<em>Because {{SITENAME}} has raw HTML enabled and you are not logged in, the preview is hidden as a precaution against JavaScript attacks.</em>\n\n<strong>If this is a legitimate preview attempt, please [[Special:UserLogin|log in]] and try again.</strong>",
"expand_templates_input_missing": "You need to provide at least some input text.",
- "pagelanguage": "Page language selector",
+ "pagelanguage": "Change page language",
"pagelang-name": "Page",
"pagelang-language": "Language",
"pagelang-use-default": "Use default language",
"pagelang-submit": "Submit",
"right-pagelang": "Change page language",
"action-pagelang": "change the page language",
- "log-name-pagelang": "Change language log",
+ "log-name-pagelang": "Language change log",
"log-description-pagelang": "This is a log of changes in page languages.",
- "logentry-pagelang-pagelang": "$1 {{GENDER:$2|changed}} page language for $3 from $4 to $5.",
+ "logentry-pagelang-pagelang": "$1 {{GENDER:$2|changed}} the language of $3 from $4 to $5",
"default-skin-not-found": "Whoops! The default skin for your wiki, defined in <code dir=\"ltr\">$wgDefaultSkin</code> as <code>$1</code>, is not available.\n\nYour installation seems to include the following {{PLURAL:$4|skin|skins}}. See [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for information how to enable {{PLURAL:$4|it|them and choose the default}}.\n\n$2\n\n; If you have just installed MediaWiki:\n: You probably installed from git, or directly from the source code using some other method. This is expected. Try installing some skins from [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org's skin directory], by:\n:* Downloading the [https://www.mediawiki.org/wiki/Download tarball installer], which comes with several skins and extensions. You can copy and paste the <code>skins/</code> directory from it.\n:* Downloading individual skin tarballs from [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Using Git to download skins].\n: Doing this should not interfere with your git repository if you're a MediaWiki developer.\n\n; If you have just upgraded MediaWiki:\n: MediaWiki 1.24 and newer no longer automatically enables installed skins (see [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). You can paste the following {{PLURAL:$5|line|lines}} into <code>LocalSettings.php</code> to enable {{PLURAL:$5|the|all}} installed {{PLURAL:$5|skin|skins}}:\n\n<pre dir=\"ltr\">$3</pre>\n\n; If you have just modified <code>LocalSettings.php</code>:\n: Double-check the skin names for typos.",
"default-skin-not-found-no-skins": "Whoops! The default skin for your wiki, defined in <code>$wgDefaultSkin</code> as <code>$1</code>, is not available.\n\nYou have no installed skins.\n\n; If you have just installed or upgraded MediaWiki:\n: You probably installed from git, or directly from the source code using some other method. This is expected. MediaWiki 1.24 and newer doesn't include any skins in the main repository. Try installing some skins from [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org's skin directory], by:\n:* Downloading the [https://www.mediawiki.org/wiki/Download tarball installer], which comes with several skins and extensions. You can copy and paste the <code>skins/</code> directory from it.\n:* Downloading individual skin tarballs from [https://www.mediawiki.org/wiki/Special:SkinDistributor mediawiki.org].\n:* [https://www.mediawiki.org/wiki/Download_from_Git#Using_Git_to_download_MediaWiki_skins Using Git to download skins].\n: Doing this should not interfere with your git repository if you're a MediaWiki developer. See [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for information how to enable skins and choose the default.\n",
"default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (enabled)",
"wlheader-showupdated": "Message at the top of [[Special:Watchlist]], after {{msg-mw|watchlist-details}}. Has to be a full sentence.",
"wlnote": "Used on [[Special:Watchlist]] when a maximum number of hours or days is specified.\n\nParameters:\n* $1 - the number of changes shown\n* $2 - the number of hours for which the changes are shown\n* $3 - a date alone\n* $4 - a time alone",
"wlshowlast": "Appears on [[Special:Watchlist]]. Parameters:\n* $1 - a choice of different numbers of hours (\"1 | 2 | 6 | 12\")\n* $2 - a choice of different numbers of days (\"1 | 3 | 7\" and the maximum number of days available)\nClicking on your choice changes the list of changes you see (without changing the default in my preferences).",
- "watchlistall2": "Appears on [[Special:Watchlist]], after {{msg-mw|wlshowtime}}, as the option to display all available data regardless of age.\n{{Identical|All}}",
"watchlist-hide": "Appears on [[Special:Watchlist]]. It is the first word on a new line with checkboxes to hide/unhide options\n{{Identical|Hide}}",
"watchlist-submit": "Label on the submit button in [[Special:Watchlist]]\n{{Identical|Show}}",
- "wlshowtime": "Appears on [[Special:Watchlist]]. Label of a drop-down list used to specify the period of time to display in the watchlist. This period can be {{msg-mw|days}}, {{msg-mw|hours}}, or {{msg-mw|watchlistall2}}.",
+ "wlshowtime": "Appears on [[Special:Watchlist]]. Label of a drop-down list used to specify the period of time to display in the watchlist. This period can be {{msg-mw|days}} or {{msg-mw|hours}}.",
"wlshowhideminor": "Option text in [[Special:Watchlist]]. Cf. {{msg-mw|rcshowhideminor}}.\n{{Identical|Minor edit}}",
"wlshowhidebots": "Option text in [[Special:Watchlist]]. Cf. {{msg-mw|rcshowhidebots}}.\n{{Identical|Bot}}",
"wlshowhideliu": "Option text in [[Special:Watchlist]]. Cf. {{msg-mw|rcshowhideliu}}.\n{{Identical|Registered user}}",
!! end
+!! test
+1. Nested mixed wikitext and html list
+!! wikitext
+* hi
+* <ul><li>ho</li></ul>
+* hi
+** ho
+!! html/php
+<ul><li> hi</li>
+<li> <ul><li>ho</li></ul></li>
+<li> hi
+<ul><li> ho</li></ul></li></ul>
+
+!! html/parsoid
+<ul><li> hi</li>
+<li> <ul data-parsoid='{"stx":"html"}'><li data-parsoid='{"stx":"html"}'>ho</li></ul></li>
+<li> hi
+<ul><li> ho</li></ul></li></ul>
+!! end
+
+!! test
+2. Nested mixed wikitext and html list (incompatible)
+!! wikitext
+; hi
+: {{echo|<li>ho</li>}}
+!! html/php
+<dl><dt> hi</dt>
+<dd> <li>ho</li></dd></dl>
+
+!! html/parsoid
+<dl><dt> hi</dt>
+<dd> <li about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","pi":[[{"k":"1","spc":["","","",""]}]]}' data-mw='{"parts":[{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"<li>ho</li>"}},"i":0}}]}'>ho</li></dd></dl>
+!! end
+
!! test
Nested lists 1
!! wikitext
</p>
!! end
+## This test is about making sure Parsoid's data-mw is well formed in the
+## face of multiple templates with intersecting and overlapping ranges. The
+## wikitext itself is wretched.
+!! test
+Templates with intersecting and overlapping ranges
+!! wikitext
+{|{{echo|
+<p>ha</p>}}
+{|{{echo|
+<p>ho</p>}}
+{{echo|{{!}}hi}}
+|}
+!! html/php+tidy
+<p>ha</p>
+<p>ho</p>
+<table>
+<tr>
+<td></td>
+</tr>
+<tr>
+<td>hi</td>
+</tr>
+</table>
+<table>
+<tr>
+<td></td>
+</tr>
+</table>
+!! html/parsoid
+<p about="#mwt1" typeof="mw:Transclusion" data-parsoid='{"stx":"html","autoInsertedEnd":true,"pi":[[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}],[{"k":"1","spc":["","","",""]}]],"firstWikitextNode":"table"}' data-mw='{"parts":["{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ha</p>"}},"i":0}},"\n","{|",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"\n<p>ho</p>"}},"i":1}},"\n",{"template":{"target":{"wt":"echo","href":"./Template:Echo"},"params":{"1":{"wt":"{{!}}hi"}},"i":2}},"\n|}"]}'>ha</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'>
+
+</table><p about="#mwt1">ho</p><table about="#mwt1" typeof="mw:ExpandedAttrs" data-mw='{"attribs":[[{"txt":"","html":""},{"html":""}]]}'>
+
+<tbody><tr><td>hi</td></tr>
+</tbody></table>
+!! end
+
!! article
Template:MSGNW test
!! text
Strip reserved data attributes
!! wikitext
<div data-mw="foo" data-parsoid="bar" data-mw-someext="baz" data-ok="fred" data-ooui="xyzzy" data-bad:ns="ns">d</div>
-!! html
+!! html/php
<div data-ok="fred">d</div>
+!! html/parsoid
+<div data-x-data-mw="foo" data-x-data-parsoid="bar" data-x-data-mw-someext="baz" data-ok="fred" data-parsoid='{"stx":"html","a":{"data-ooui":null,"data-bad:ns":null},"sa":{"data-ooui":"xyzzy","data-bad:ns":"ns"}}'>d</div>
!! end
!! test
}
class WellProtectedClass extends WellProtectedParentClass {
+ protected static $staticProperty = 'sp';
+ private static $staticPrivateProperty = 'spp';
+
protected $property;
private $privateProperty;
+ protected static function staticMethod() {
+ return 'sm';
+ }
+
+ private static function staticPrivateMethod() {
+ return 'spm';
+ }
+
public function __construct() {
parent::__construct();
$this->property = 1;
* $formatter = $title->getTitleFormatter();
*
* TODO:
- * - Provide access to static methods and properties.
* - Organize other helper classes in tests/testHelpers.inc into a directory.
*/
class TestingAccessWrapper {
+ /** @var mixed The object, or the class name for static-only access */
public $object;
/**
* Return the same object, without access restrictions.
*/
public static function newFromObject( $object ) {
+ if ( !is_object( $object ) ) {
+ throw new InvalidArgumentException( __METHOD__ . ' must be called with an object' );
+ }
$wrapper = new TestingAccessWrapper();
$wrapper->object = $object;
return $wrapper;
}
+ /**
+ * Allow access to non-public static methods and properties of the class.
+ * Use non-static access,
+ */
+ public static function newFromClass( $className ) {
+ if ( !is_string( $className ) ) {
+ throw new InvalidArgumentException( __METHOD__ . ' must be called with a class name' );
+ }
+ $wrapper = new TestingAccessWrapper();
+ $wrapper->object = $className;
+ return $wrapper;
+ }
+
public function __call( $method, $args ) {
+ $methodReflection = $this->getMethod( $method );
+
+ if ( $this->isStatic() && !$methodReflection->isStatic() ) {
+ throw new DomainException( __METHOD__ . ': Cannot call non-static when wrapping static class' );
+ }
+
+ return $methodReflection->invokeArgs( $methodReflection->isStatic() ? null : $this->object,
+ $args );
+ }
+
+ public function __set( $name, $value ) {
+ $propertyReflection = $this->getProperty( $name );
+
+ if ( $this->isStatic() && !$propertyReflection->isStatic() ) {
+ throw new DomainException( __METHOD__ . ': Cannot set property when wrapping static class' );
+ }
+
+ $propertyReflection->setValue( $this->object, $value );
+ }
+
+ public function __get( $name ) {
+ $propertyReflection = $this->getProperty( $name );
+
+ if ( $this->isStatic() && !$propertyReflection->isStatic() ) {
+ throw new DomainException( __METHOD__ . ': Cannot get property when wrapping static class' );
+ }
+
+ return $propertyReflection->getValue( $this->object );
+ }
+
+ private function isStatic() {
+ return is_string( $this->object );
+ }
+
+ /**
+ * Return a property and make it accessible.
+ * @param string $name
+ * @return ReflectionMethod
+ */
+ private function getMethod( $name ) {
$classReflection = new ReflectionClass( $this->object );
- $methodReflection = $classReflection->getMethod( $method );
+ $methodReflection = $classReflection->getMethod( $name );
$methodReflection->setAccessible( true );
- return $methodReflection->invokeArgs( $this->object, $args );
+ return $methodReflection;
}
/**
+ * Return a property and make it accessible.
+ *
* ReflectionClass::getProperty() fails if the private property is defined
* in a parent class. This works more like ReflectionClass::getMethod().
+ *
+ * @param string $name
+ * @return ReflectionProperty
+ * @throws ReflectionException
*/
private function getProperty( $name ) {
$classReflection = new ReflectionClass( $this->object );
try {
- return $classReflection->getProperty( $name );
+ $propertyReflection = $classReflection->getProperty( $name );
} catch ( ReflectionException $ex ) {
while ( true ) {
$classReflection = $classReflection->getParentClass();
continue;
}
if ( $propertyReflection->isPrivate() ) {
- return $propertyReflection;
+ break;
} else {
throw $ex;
}
}
}
- }
-
- public function __set( $name, $value ) {
- $propertyReflection = $this->getProperty( $name );
$propertyReflection->setAccessible( true );
- $propertyReflection->setValue( $this->object, $value );
- }
-
- public function __get( $name ) {
- $propertyReflection = $this->getProperty( $name );
- $propertyReflection->setAccessible( true );
- return $propertyReflection->getValue( $this->object );
+ return $propertyReflection;
}
}
class TestingAccessWrapperTest extends MediaWikiTestCase {
protected $raw;
protected $wrapped;
+ protected $wrappedStatic;
function setUp() {
parent::setUp();
require_once __DIR__ . '/../data/helpers/WellProtectedClass.php';
$this->raw = new WellProtectedClass();
$this->wrapped = TestingAccessWrapper::newFromObject( $this->raw );
+ $this->wrappedStatic = TestingAccessWrapper::newFromClass( 'WellProtectedClass' );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ function testConstructorException() {
+ TestingAccessWrapper::newFromObject( 'WellProtectedClass' );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ function testStaticConstructorException() {
+ TestingAccessWrapper::newFromClass( new WellProtectedClass() );
}
function testGetProperty() {
$this->assertSame( 1, $this->wrapped->property );
$this->assertSame( 42, $this->wrapped->privateProperty );
$this->assertSame( 9000, $this->wrapped->privateParentProperty );
+ $this->assertSame( 'sp', $this->wrapped->staticProperty );
+ $this->assertSame( 'spp', $this->wrapped->staticPrivateProperty );
+ $this->assertSame( 'sp', $this->wrappedStatic->staticProperty );
+ $this->assertSame( 'spp', $this->wrappedStatic->staticPrivateProperty );
+ }
+
+ /**
+ * @expectedException DomainException
+ */
+ function testGetException() {
+ $this->wrappedStatic->property;
}
function testSetProperty() {
$this->wrapped->privateParentProperty = 12;
$this->assertSame( 12, $this->wrapped->privateParentProperty );
$this->assertSame( 12, $this->raw->getPrivateParentProperty() );
+
+ $this->wrapped->staticProperty = 'x';
+ $this->assertSame( 'x', $this->wrapped->staticProperty );
+ $this->assertSame( 'x', $this->wrappedStatic->staticProperty );
+
+ $this->wrapped->staticPrivateProperty = 'y';
+ $this->assertSame( 'y', $this->wrapped->staticPrivateProperty );
+ $this->assertSame( 'y', $this->wrappedStatic->staticPrivateProperty );
+
+ $this->wrappedStatic->staticProperty = 'X';
+ $this->assertSame( 'X', $this->wrapped->staticProperty );
+ $this->assertSame( 'X', $this->wrappedStatic->staticProperty );
+
+ $this->wrappedStatic->staticPrivateProperty = 'Y';
+ $this->assertSame( 'Y', $this->wrapped->staticPrivateProperty );
+ $this->assertSame( 'Y', $this->wrappedStatic->staticPrivateProperty );
+
+ // don't rely on PHPUnit to restore static properties
+ $this->wrapped->staticProperty = 'sp';
+ $this->wrapped->staticPrivateProperty = 'spp';
+ }
+
+ /**
+ * @expectedException DomainException
+ */
+ function testSetException() {
+ $this->wrappedStatic->property = 1;
}
function testCallMethod() {
$this->wrapped->incrementPrivateParentPropertyValue();
$this->assertSame( 9001, $this->wrapped->privateParentProperty );
$this->assertSame( 9001, $this->raw->getPrivateParentProperty() );
+
+ $this->assertSame( 'sm', $this->wrapped->staticMethod() );
+ $this->assertSame( 'spm', $this->wrapped->staticPrivateMethod() );
+ $this->assertSame( 'sm', $this->wrappedStatic->staticMethod() );
+ $this->assertSame( 'spm', $this->wrappedStatic->staticPrivateMethod() );
}
function testCallMethodTwoArgs() {
$this->assertSame( 'two', $this->wrapped->whatSecondArg( 'one', 'two' ) );
}
+
+ /**
+ * @expectedException DomainException
+ */
+ function testCallMethodException() {
+ $this->wrappedStatic->incrementPropertyValue();
+ }
+
}
),
),
array(
- 'text' => 'User changed page language for Page from English (en) to Deutsch (de) [default].',
+ 'text' => 'User changed the language of Page from English (en) to Deutsch (de) [default]',
'api' => array(
'oldlanguage' => 'en',
'newlanguage' => 'de[def]'
'cookieOptions' => array( 'prefix' => 'x' ),
) );
$config = $this->getConfig();
- $config->set( 'CookieSecure', false );
+ $config->set( 'CookieSecure', $secure );
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
$provider->setManager( SessionManager::singleton() );