if ( $cpIndex > 0 ) {
if ( $allowHeaders ) {
- $expires = time() + ChronologyProtector::POSITION_TTL;
+ $now = time();
+ $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL;
$options = [ 'prefix' => '' ];
- $request->response()->setCookie( 'cpPosIndex', $cpIndex, $expires, $options );
+ $value = LBFactory::makeCookieValueFromCPIndex( $cpIndex, $now ); // T190082
+ $request->response()->setCookie( 'cpPosIndex', $value, $expires, $options );
}
if ( $strategy === 'cookie+url' ) {
* @file
*/
use MediaWiki\MediaWikiServices;
+use Wikimedia\Rdbms\LBFactory;
+use Wikimedia\Rdbms\ChronologyProtector;
/**
* This file is not a valid entry point, perform no further processing unless
'IPAddress' => $wgRequest->getIP(),
'UserAgent' => $wgRequest->getHeader( 'User-Agent' ),
'ChronologyProtection' => $wgRequest->getHeader( 'ChronologyProtection' ),
- // The cpPosIndex cookie has no prefix and is set by MediaWiki::preOutputCommit()
- 'ChronologyPositionIndex' =>
- $wgRequest->getInt( 'cpPosIndex', (int)$wgRequest->getCookie( 'cpPosIndex', '' ) )
+ 'ChronologyPositionIndex' => $wgRequest->getInt(
+ 'cpPosIndex',
+ LBFactory::getCPIndexFromCookieValue(
+ // The cookie has no prefix and is set by MediaWiki::preOutputCommit()
+ $wgRequest->getCookie( 'cpPosIndex', '' ),
+ // Mitigate broken client-side cookie expiration handling (T190082)
+ time() - ChronologyProtector::POSITION_COOKIE_TTL
+ )
+ )
] );
// Make sure that object caching does not undermine the ChronologyProtector improvements
if ( $wgRequest->getCookie( 'UseDC', '' ) === 'master' ) {
/** @var int Seconds to store positions */
const POSITION_TTL = 60;
+ /** @var int Seconds to store position write index cookies (safely less than POSITION_TTL) */
+ const POSITION_COOKIE_TTL = 60;
/** @var int Max time to wait for positions to appear */
const POS_STORE_WAIT_TIMEOUT = 5;
* Note that unlike cookies, this works accross domains
*
* @param string $url
- * @param float $time UNIX timestamp just before shutdown() was called
+ * @param int $index Write counter index
* @return string
*/
- public function appendShutdownCPIndexAsQuery( $url, $time );
+ public function appendShutdownCPIndexAsQuery( $url, $index );
/**
* @param array $info Map of fields, including:
return strpos( $url, '?' ) === false ? "$url?cpPosIndex=$index" : "$url&cpPosIndex=$index";
}
+ /**
+ * @param int $index Write index
+ * @param int $time UNIX timestamp
+ * @return string Timestamp-qualified write index of the form "<index>.<timestamp>"
+ * @since 1.32
+ */
+ public static function makeCookieValueFromCPIndex( $index, $time ) {
+ return $index . '@' . $time;
+ }
+
+ /**
+ * @param string $value String possibly of the form "<index>" or "<index>@<timestamp>"
+ * @param int $minTimestamp Lowest UNIX timestamp of non-expired values (if present)
+ * @return int|null Write index or null if $value is empty or expired
+ * @since 1.32
+ */
+ public static function getCPIndexFromCookieValue( $value, $minTimestamp ) {
+ if ( !preg_match( '/^(\d+)(?:@(\d+))?$/', $value, $m ) ) {
+ return null;
+ }
+
+ $index = (int)$m[1];
+
+ if ( isset( $m[2] ) && $m[2] !== '' && (int)$m[2] < $minTimestamp ) {
+ return null; // expired
+ }
+
+ return ( $index > 0 ) ? $index : null;
+ }
+
public function setRequestInfo( array $info ) {
if ( $this->chronProt ) {
throw new LogicException( 'ChronologyProtector already initialized.' );
return $db->addIdentifierQuotes( $table );
}
}
+
+ /**
+ * @covers \Wikimedia\Rdbms\LBFactory::makeCookieValueFromCPIndex()
+ * @covers \Wikimedia\Rdbms\LBFactory::getCPIndexFromCookieValue()
+ */
+ public function testCPPosIndexCookieValues() {
+ $this->assertEquals( '3@542', LBFactory::makeCookieValueFromCPIndex( 3, 542 ) );
+
+ $time = 1526522031;
+ $this->assertSame(
+ 5,
+ LBFactory::getCPIndexFromCookieValue( "5", $time - 10 )
+ );
+ $this->assertSame(
+ null,
+ LBFactory::getCPIndexFromCookieValue( "0", $time - 10 )
+ );
+ $this->assertSame(
+ 2,
+ LBFactory::getCPIndexFromCookieValue( "2@$time", $time - 10 )
+ );
+ $this->assertSame(
+ 2,
+ LBFactory::getCPIndexFromCookieValue( "2@$time", $time + 9 - 10 )
+ );
+ $this->assertSame(
+ null,
+ LBFactory::getCPIndexFromCookieValue( "0@$time", $time + 9 - 10 )
+ );
+ $this->assertSame(
+ null,
+ LBFactory::getCPIndexFromCookieValue( "2@$time", $time + 11 - 10 )
+ );
+ }
}