diff --git a/autoload.php b/autoload.php index 36773746b09..fbdee83ba86 100644 --- a/autoload.php +++ b/autoload.php @@ -1165,6 +1165,7 @@ $wgAutoloadLocalClasses = [ 'RESTBagOStuff' => __DIR__ . '/includes/libs/objectcache/RESTBagOStuff.php', 'RSSFeed' => __DIR__ . '/includes/Feed.php', 'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php', + 'RangeChronologicalPager' => __DIR__ . '/includes/pager/RangeChronologicalPager.php', 'RangeDifference' => __DIR__ . '/includes/diff/DiffEngine.php', 'RawAction' => __DIR__ . '/includes/actions/RawAction.php', 'RawMessage' => __DIR__ . '/includes/Message.php', diff --git a/includes/pager/RangeChronologicalPager.php b/includes/pager/RangeChronologicalPager.php new file mode 100644 index 00000000000..901d576df96 --- /dev/null +++ b/includes/pager/RangeChronologicalPager.php @@ -0,0 +1,121 @@ +rangeConds = []; + + try { + if ( $startStamp !== '' ) { + $startTimestamp = MWTimestamp::getInstance( $startStamp ); + $startTimestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) ); + $startOffset = $this->mDb->timestamp( $startTimestamp->getTimestamp() ); + $this->rangeConds[] = $this->mIndexField . '>=' . $this->mDb->addQuotes( $startOffset ); + } + + if ( $endStamp !== '' ) { + $endTimestamp = MWTimestamp::getInstance( $endStamp ); + $endTimestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) ); + $endOffset = $this->mDb->timestamp( $endTimestamp->getTimestamp() ); + $this->rangeConds[] = $this->mIndexField . '<=' . $this->mDb->addQuotes( $endOffset ); + + // populate existing variables for compatibility with parent + $this->mYear = (int)$endTimestamp->format( 'Y' ); + $this->mMonth = (int)$endTimestamp->format( 'm' ); + $this->mDay = (int)$endTimestamp->format( 'd' ); + $this->mOffset = $endOffset; + } + } catch ( TimestampException $ex ) { + return null; + } + + return $this->rangeConds; + } + + /** + * Takes ReverseChronologicalPager::getDateCond parameters and repurposes + * them to work with timestamp-based getDateRangeCond. + * + * @param int $year Year up to which we want revisions + * @param int $month Month up to which we want revisions + * @param int $day [optional] Day up to which we want revisions. Default is end of month. + * @return string|null Timestamp or null if year and month are false/invalid + */ + public function getDateCond( $year, $month, $day = -1 ) { + // run through getDateRangeCond so rangeConds, mOffset, ... are set + $legacyTimestamp = self::getOffsetDate( $year, $month, $day ); + // ReverseChronologicalPager uses strict inequality for the end date ('<'), + // but this class uses '<=' and expects extending classes to handle modifying the end date. + // Therefore, we need to subtract one second from the output of getOffsetDate to make it + // work with the '<=' inequality used in this class. + $legacyTimestamp->timestamp = $legacyTimestamp->timestamp->modify( '-1 second' ); + $this->getDateRangeCond( '', $legacyTimestamp->getTimestamp( TS_MW ) ); + return $this->mOffset; + } + + /** + * Build variables to use by the database wrapper. + * + * @param string $offset Index offset, inclusive + * @param int $limit Exact query limit + * @param bool $descending Query direction, false for ascending, true for descending + * @return array + */ + protected function buildQueryInfo( $offset, $limit, $descending ) { + if ( count( $this->rangeConds ) > 0 ) { + // If range conditions are set, $offset is not used. + // However, if range conditions aren't set, (such as when using paging links) + // use the provided offset to get the proper query. + $offset = ''; + } + + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = parent::buildQueryInfo( + $offset, + $limit, + $descending + ); + + if ( $this->rangeConds ) { + $conds = array_merge( $conds, $this->rangeConds ); + } + + return [ $tables, $fields, $conds, $fname, $options, $join_conds ]; + } +} diff --git a/includes/pager/ReverseChronologicalPager.php b/includes/pager/ReverseChronologicalPager.php index 76f347023e8..9eef728a933 100644 --- a/includes/pager/ReverseChronologicalPager.php +++ b/includes/pager/ReverseChronologicalPager.php @@ -1,7 +1,5 @@ isNavigationBarShown() ) { return ''; } @@ -65,52 +64,79 @@ abstract class ReverseChronologicalPager extends IndexPager { /** * Set and return the mOffset timestamp such that we can get all revisions with * a timestamp up to the specified parameters. + * * @param int $year Year up to which we want revisions * @param int $month Month up to which we want revisions * @param int $day [optional] Day up to which we want revisions. Default is end of month. * @return string|null Timestamp or null if year and month are false/invalid */ - function getDateCond( $year, $month, $day = -1 ) { - $year = intval( $year ); - $month = intval( $month ); - $day = intval( $day ); + public function getDateCond( $year, $month, $day = -1 ) { + $year = (int)$year; + $month = (int)$month; + $day = (int)$day; // Basic validity checks for year and month - $this->mYear = $year > 0 ? $year : false; - $this->mMonth = ( $month > 0 && $month < 13 ) ? $month : false; - - // If year and month are false, don't update the mOffset - if ( !$this->mYear && !$this->mMonth ) { + // If year and month are invalid, don't update the mOffset + if ( $year <= 0 && ( $month <= 0 || $month >= 13 ) ) { return null; } + // Treat the given time in the wiki timezone and get a UTC timestamp for the database lookup + $timestamp = self::getOffsetDate( $year, $month, $day ); + $timestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) ); + + try { + $this->mYear = (int)$timestamp->format( 'Y' ); + $this->mMonth = (int)$timestamp->format( 'm' ); + $this->mDay = (int)$timestamp->format( 'd' ); + $this->mOffset = $this->mDb->timestamp( $timestamp->getTimestamp() ); + } catch ( TimestampException $e ) { + // Invalid user provided timestamp (T149257) + return null; + } + + return $this->mOffset; + } + + /** + * Core logic of determining the mOffset timestamp such that we can get all items with + * a timestamp up to the specified parameters. Given parameters for a day up to which to get + * items, this function finds the timestamp of the day just after the end of the range for use + * in an database strict inequality filter. + * + * This is separate from getDateCond so we can use this logic in other places, such as in + * RangeChronologicalPager, where this function is used to convert year/month/day filter options + * into a timestamp. + * + * @param int $year Year up to which we want revisions + * @param int $month Month up to which we want revisions + * @param int $day [optional] Day up to which we want revisions. Default is end of month. + * @return MWTimestamp Timestamp or null if year and month are false/invalid + */ + public static function getOffsetDate( $year, $month, $day = -1 ) { // Given an optional year, month, and day, we need to generate a timestamp // to use as "WHERE rev_timestamp <= result" // Examples: year = 2006 equals < 20070101 (+000000) // year=2005, month=1 equals < 20050201 // year=2005, month=12 equals < 20060101 // year=2005, month=12, day=5 equals < 20051206 - if ( $this->mYear ) { - $year = $this->mYear; - } else { + if ( $year <= 0 ) { // If no year given, assume the current one $timestamp = MWTimestamp::getInstance(); $year = $timestamp->format( 'Y' ); // If this month hasn't happened yet this year, go back to last year's month - if ( $this->mMonth > $timestamp->format( 'n' ) ) { + if ( $month > $timestamp->format( 'n' ) ) { $year--; } } - if ( $this->mMonth ) { - $month = $this->mMonth; - + if ( $month && $month > 0 && $month < 13 ) { // Day validity check after we have month and year checked - $this->mDay = checkdate( $month, $day, $year ) ? $day : false; + $day = checkdate( $month, $day, $year ) ? $day : false; - if ( $this->mDay ) { + if ( $day && $day > 0 ) { // If we have a day, we want up to the day immediately afterward - $day = $this->mDay + 1; + $day++; // Did we overflow the current month? if ( !checkdate( $month, $day, $year ) ) { @@ -147,17 +173,6 @@ abstract class ReverseChronologicalPager extends IndexPager { $ymd = 20320101; } - // Treat the given time in the wiki timezone and get a UTC timestamp for the database lookup - $timestamp = MWTimestamp::getInstance( "${ymd}000000" ); - $timestamp->setTimezone( $this->getConfig()->get( 'Localtimezone' ) ); - - try { - $this->mOffset = $this->mDb->timestamp( $timestamp->getTimestamp() ); - } catch ( TimestampException $e ) { - // Invalid user provided timestamp (T149257) - return null; - } - - return $this->mOffset; + return MWTimestamp::getInstance( "${ymd}000000" ); } } diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 05164df522f..15e82b6153b 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4180,6 +4180,8 @@ "mw-widgets-titleinput-description-redirect": "redirect to $1", "mw-widgets-categoryselector-add-category-placeholder": "Add a category...", "mw-widgets-usersmultiselect-placeholder": "Add more...", + "date-range-from": "From date:", + "date-range-to": "To date:", "sessionmanager-tie": "Cannot combine multiple request authentication types: $1.", "sessionprovider-generic": "$1 sessions", "sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-based sessions", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 0234d247595..cd42665a33d 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4368,6 +4368,8 @@ "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.", "mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.", "mw-widgets-usersmultiselect-placeholder": "Placeholder displayed in the input field, where new usernames are entered", + "date-range-from": "Label for an input field that specifies the start date of a date range filter.", + "date-range-to": " Label for an input field that specifies the end date of a date range filter.", "sessionmanager-tie": "Used as an error message when multiple session sources are tied in priority.\n\nParameters:\n* $1 - List of dession type descriptions, from messages like {{msg-mw|sessionprovider-mediawiki-session-cookiesessionprovider}}.", "sessionprovider-generic": "Used to create a generic session type description when one isn't provided via the proper message. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.\n\nParameters:\n* $1 - PHP classname.", "sessionprovider-mediawiki-session-cookiesessionprovider": "Description of the sessions provided by the CookieSessionProvider class, which use HTTP cookies. Should be phrased to make sense when added to a message such as {{msg-mw|cannotloginnow-text}}.", diff --git a/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php b/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php new file mode 100644 index 00000000000..3374f4ab939 --- /dev/null +++ b/tests/phpunit/includes/pager/RangeChronologicalPagerTest.php @@ -0,0 +1,96 @@ + + */ +class RangeChronologicalPagerTest extends MediaWikiLangTestCase { + + /** + * @covers RangeChronologicalPager::getDateCond + * @dataProvider getDateCondProvider + */ + public function testGetDateCond( $inputYear, $inputMonth, $inputDay, $expected ) { + $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $this->assertEquals( $expected, $pager->getDateCond( $inputYear, $inputMonth, $inputDay ) ); + } + + /** + * Data provider in [ input year, input month, input day, expected timestamp output ] format + */ + public function getDateCondProvider() { + return [ + [ 2016, 12, 5, '20161205235959' ], + [ 2016, 12, 31, '20161231235959' ], + [ 2016, 12, 1337, '20161231235959' ], + [ 2016, 1337, 1337, '20161231235959' ], + [ 2016, 1337, -1, '20161231235959' ], + [ 2016, 12, 32, '20161231235959' ], + [ 2016, 12, -1, '20161231235959' ], + [ 2016, -1, -1, '20161231235959' ], + ]; + } + + /** + * @covers RangeChronologicalPager::getDateRangeCond + * @dataProvider getDateRangeCondProvider + */ + public function testGetDateRangeCond( $start, $end, $expected ) { + $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $this->assertArrayEquals( $expected, $pager->getDateRangeCond( $start, $end ) ); + } + + /** + * Data provider in [ start, end, [ expected output has start condition, has end cond ] ] format + */ + public function getDateRangeCondProvider() { + $db = wfGetDB( DB_MASTER ); + + return [ + [ + '20161201000000', + '20161203000000', + [ + '>=' . $db->addQuotes( $db->timestamp( '20161201000000' ) ), + '<=' . $db->addQuotes( $db->timestamp( '20161203000000' ) ), + ], + ], + [ + '', + '20161203000000', + [ + '<=' . $db->addQuotes( $db->timestamp( '20161203000000' ) ), + ], + ], + [ + '20161201000000', + '', + [ + '>=' . $db->addQuotes( $db->timestamp( '20161201000000' ) ), + ], + ], + [ '', '', [] ], + ]; + } + + /** + * @covers RangeChronologicalPager::getDateRangeCond + * @dataProvider getDateRangeCondInvalidProvider + */ + public function testGetDateRangeCondInvalid( $start, $end ) { + $pager = $this->getMockForAbstractClass( 'RangeChronologicalPager' ); + $this->assertEquals( null, $pager->getDateRangeCond( $start, $end ) ); + } + + public function getDateRangeCondInvalidProvider() { + return [ + [ '-2016-12-01', '2017-12-01', ], + [ '2016-12-01', '-2017-12-01', ], + [ 'abcdefghij', 'klmnopqrstu', ], + ]; + } + +}