wiki.techinc.nl/includes/specials/SpecialRandomInCategory.php
Umherirrender 40738ec59f Inject services into SpecialRandom and friends
This covers only directly used services by this special page and pager
Services used by the base class are not part of this patch set

Bug: T259960
Change-Id: Ideea375c1698d111a44049b1f460c8fe64c3119c
2020-11-02 02:45:14 +01:00

326 lines
9 KiB
PHP

<?php
/**
* Implements Special:RandomInCategory
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup SpecialPage
* @author Brian Wolff
*/
use Wikimedia\Rdbms\ILoadBalancer;
/**
* Special page to direct the user to a random page
*
* @note The method used here is rather biased. It is assumed that
* the use of this page will be people wanting to get a random page
* out of a maintenance category, to fix it up. The method used by
* this page should return different pages in an unpredictable fashion
* which is hoped to be sufficient, even if some pages are selected
* more often than others.
*
* A more unbiased method could be achieved by adding a cl_random field
* to the categorylinks table.
*
* The method used here is as follows:
* * Find the smallest and largest timestamp in the category
* * Pick a random timestamp in between
* * Pick an offset between 0 and 30
* * Get the offset'ed page that is newer than the timestamp selected
* The offset is meant to counter the fact the timestamps aren't usually
* uniformly distributed, so if things are very non-uniform at least we
* won't have the same page selected 99% of the time.
*
* @ingroup SpecialPage
*/
class SpecialRandomInCategory extends FormSpecialPage {
/** @var string[] */
protected $extra = []; // Extra SQL statements
/** @var Title|false */
protected $category = false; // Title object of category
/** @var int */
protected $maxOffset = 30; // Max amount to fudge randomness by.
/** @var int|null */
private $maxTimestamp = null;
/** @var int|null */
private $minTimestamp = null;
/** @var ILoadBalancer */
private $loadBalancer;
/**
* @param ILoadBalancer $loadBalancer
*/
public function __construct( ILoadBalancer $loadBalancer ) {
parent::__construct( 'RandomInCategory' );
$this->loadBalancer = $loadBalancer;
}
/**
* Set which category to use.
* @param Title $cat
*/
public function setCategory( Title $cat ) {
$this->category = $cat;
$this->maxTimestamp = null;
$this->minTimestamp = null;
}
protected function getFormFields() {
$this->addHelpLink( 'Help:RandomInCategory' );
return [
'category' => [
'type' => 'title',
'namespace' => NS_CATEGORY,
'relative' => true,
'label-message' => 'randomincategory-category',
'required' => true,
]
];
}
public function requiresWrite() {
return false;
}
public function requiresUnblock() {
return false;
}
protected function getDisplayFormat() {
return 'ooui';
}
protected function alterForm( HTMLForm $form ) {
$form->setSubmitTextMsg( 'randomincategory-submit' );
}
protected function setParameter( $par ) {
// if subpage present, fake form submission
$this->onSubmit( [ 'category' => $par ] );
}
public function onSubmit( array $data ) {
$cat = false;
$categoryStr = $data['category'];
if ( $categoryStr ) {
$cat = Title::newFromText( $categoryStr, NS_CATEGORY );
}
if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
// Someone searching for something like "Wikipedia:Foo"
$cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
}
if ( $cat ) {
$this->setCategory( $cat );
}
if ( !$this->category && $categoryStr ) {
$msg = $this->msg( 'randomincategory-invalidcategory',
wfEscapeWikiText( $categoryStr ) );
return Status::newFatal( $msg );
} elseif ( !$this->category ) {
return false; // no data sent
}
$title = $this->getRandomTitle();
if ( $title === null ) {
$msg = $this->msg( 'randomincategory-nopages',
$this->category->getText() );
return Status::newFatal( $msg );
}
$query = $this->getRequest()->getValues();
unset( $query['title'] );
$this->getOutput()->redirect( $title->getFullURL( $query ) );
}
/**
* Choose a random title.
* @return Title|null Title object (or null if nothing to choose from)
*/
public function getRandomTitle() {
// Convert to float, since we do math with the random number.
$rand = (float)wfRandom();
$title = null;
// Given that timestamps are rather unevenly distributed, we also
// use an offset between 0 and 30 to make any biases less noticeable.
$offset = mt_rand( 0, $this->maxOffset );
if ( mt_rand( 0, 1 ) ) {
$up = true;
} else {
$up = false;
}
$row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
// Try again without the timestamp offset (wrap around the end)
if ( !$row ) {
$row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
}
// Maybe the category is really small and offset too high
if ( !$row ) {
$row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
}
// Just get the first entry.
if ( !$row ) {
$row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
}
if ( $row ) {
return Title::makeTitle( $row->page_namespace, $row->page_title );
}
return null;
}
/**
* @param float $rand Random number between 0 and 1
* @param int $offset Extra offset to fudge randomness
* @param bool $up True to get the result above the random number, false for below
* @return array Query information.
* @throws MWException
* @note The $up parameter is supposed to counteract what would happen if there
* was a large gap in the distribution of cl_timestamp values. This way instead
* of things to the right of the gap being favoured, both sides of the gap
* are favoured.
*/
protected function getQueryInfo( $rand, $offset, $up ) {
$op = $up ? '>=' : '<=';
$dir = $up ? 'ASC' : 'DESC';
if ( !$this->category instanceof Title ) {
throw new MWException( 'No category set' );
}
$qi = [
'tables' => [ 'categorylinks', 'page' ],
'fields' => [ 'page_title', 'page_namespace' ],
'conds' => array_merge( [
'cl_to' => $this->category->getDBkey(),
], $this->extra ),
'options' => [
'ORDER BY' => 'cl_timestamp ' . $dir,
'LIMIT' => 1,
'OFFSET' => $offset
],
'join_conds' => [
'page' => [ 'JOIN', 'cl_from = page_id' ]
]
];
$dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
$minClTime = $this->getTimestampOffset( $rand );
if ( $minClTime ) {
$qi['conds'][] = 'cl_timestamp ' . $op . ' ' .
$dbr->addQuotes( $dbr->timestamp( $minClTime ) );
}
return $qi;
}
/**
* @param float $rand Random number between 0 and 1
*
* @return int|bool A random (unix) timestamp from the range of the category or false on failure
*/
protected function getTimestampOffset( $rand ) {
if ( $rand === false ) {
return false;
}
if ( !$this->minTimestamp || !$this->maxTimestamp ) {
try {
list( $this->minTimestamp, $this->maxTimestamp ) = $this->getMinAndMaxForCat( $this->category );
} catch ( Exception $e ) {
// Possibly no entries in category.
return false;
}
}
$ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
return intval( $ts );
}
/**
* Get the lowest and highest timestamp for a category.
*
* @param Title $category
* @return array The lowest and highest timestamp
* @throws MWException If category has no entries.
*/
protected function getMinAndMaxForCat( Title $category ) {
$dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
$res = $dbr->selectRow(
'categorylinks',
[
'low' => 'MIN( cl_timestamp )',
'high' => 'MAX( cl_timestamp )'
],
[
'cl_to' => $this->category->getDBkey(),
],
__METHOD__,
[
'LIMIT' => 1
]
);
if ( !$res ) {
throw new MWException( 'No entries in category' );
}
return [ wfTimestamp( TS_UNIX, $res->low ), wfTimestamp( TS_UNIX, $res->high ) ];
}
/**
* @param float $rand A random number that is converted to a random timestamp
* @param int $offset A small offset to make the result seem more "random"
* @param bool $up Get the result above the random value
* @param string $fname The name of the calling method
* @return stdClass|false Info for the title selected.
*/
private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
$dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
$query = $this->getQueryInfo( $rand, $offset, $up );
$res = $dbr->select(
$query['tables'],
$query['fields'],
$query['conds'],
$fname,
$query['options'],
$query['join_conds']
);
return $res->fetchObject();
}
protected function getGroupName() {
return 'redirects';
}
}