MediaWiki  master
SpecialRandomInCategory.php
Go to the documentation of this file.
1 <?php
25 namespace MediaWiki\Specials;
26 
27 use BadMethodCallException;
28 use HTMLForm;
32 use stdClass;
35 
62  protected $extra = []; // Extra SQL statements
64  protected $category = false; // Title object of category
66  protected $maxOffset = 30; // Max amount to fudge randomness by.
68  private $maxTimestamp = null;
70  private $minTimestamp = null;
71 
72  private IConnectionProvider $dbProvider;
73 
77  public function __construct( IConnectionProvider $dbProvider ) {
78  parent::__construct( 'RandomInCategory' );
79  $this->dbProvider = $dbProvider;
80  }
81 
86  public function setCategory( Title $cat ) {
87  $this->category = $cat;
88  $this->maxTimestamp = null;
89  $this->minTimestamp = null;
90  }
91 
92  protected function getFormFields() {
93  $this->addHelpLink( 'Help:RandomInCategory' );
94 
95  return [
96  'category' => [
97  'type' => 'title',
98  'namespace' => NS_CATEGORY,
99  'relative' => true,
100  'label-message' => 'randomincategory-category',
101  'required' => true,
102  ]
103  ];
104  }
105 
106  public function requiresPost() {
107  return false;
108  }
109 
110  protected function getDisplayFormat() {
111  return 'ooui';
112  }
113 
114  protected function alterForm( HTMLForm $form ) {
115  $form->setSubmitTextMsg( 'randomincategory-submit' );
116  }
117 
118  protected function getSubpageField() {
119  return 'category';
120  }
121 
122  public function onSubmit( array $data ) {
123  $cat = false;
124 
125  $categoryStr = $data['category'];
126 
127  if ( $categoryStr ) {
128  $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
129  }
130 
131  if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
132  // Someone searching for something like "Wikipedia:Foo"
133  $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
134  }
135 
136  if ( $cat ) {
137  $this->setCategory( $cat );
138  }
139 
140  if ( !$this->category && $categoryStr ) {
141  $msg = $this->msg( 'randomincategory-invalidcategory',
142  wfEscapeWikiText( $categoryStr ) );
143 
144  return Status::newFatal( $msg );
145 
146  } elseif ( !$this->category ) {
147  return false; // no data sent
148  }
149 
150  $title = $this->getRandomTitle();
151 
152  if ( $title === null ) {
153  $msg = $this->msg( 'randomincategory-nopages',
154  $this->category->getText() );
155 
156  return Status::newFatal( $msg );
157  }
158 
159  $query = $this->getRequest()->getQueryValues();
160  unset( $query['title'] );
161  $this->getOutput()->redirect( $title->getFullURL( $query ) );
162  }
163 
168  public function getRandomTitle() {
169  // Convert to float, since we do math with the random number.
170  $rand = (float)wfRandom();
171 
172  // Given that timestamps are rather unevenly distributed, we also
173  // use an offset between 0 and 30 to make any biases less noticeable.
174  $offset = mt_rand( 0, $this->maxOffset );
175 
176  if ( mt_rand( 0, 1 ) ) {
177  $up = true;
178  } else {
179  $up = false;
180  }
181 
182  $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
183 
184  // Try again without the timestamp offset (wrap around the end)
185  if ( !$row ) {
186  $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
187  }
188 
189  // Maybe the category is really small and offset too high
190  if ( !$row ) {
191  $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
192  }
193 
194  // Just get the first entry.
195  if ( !$row ) {
196  $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
197  }
198 
199  if ( $row ) {
200  return Title::makeTitle( $row->page_namespace, $row->page_title );
201  }
202 
203  return null;
204  }
205 
216  protected function getQueryBuilder( $rand, $offset, $up ) {
217  $op = $up ? '>=' : '<=';
218  if ( !$this->category instanceof Title ) {
219  throw new BadMethodCallException( 'No category set' );
220  }
221  $dbr = $this->dbProvider->getReplicaDatabase();
222  $queryBuilder = $dbr->newSelectQueryBuilder()
223  ->select( [ 'page_title', 'page_namespace' ] )
224  ->from( 'categorylinks' )
225  ->join( 'page', null, 'cl_from = page_id' )
226  ->where( [ 'cl_to' => $this->category->getDBkey() ] )
227  ->andWhere( $this->extra )
228  ->orderBy( 'cl_timestamp', $up ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC )
229  ->limit( 1 )
230  ->offset( $offset );
231 
232  $minClTime = $this->getTimestampOffset( $rand );
233  if ( $minClTime ) {
234  $queryBuilder->andWhere(
235  $dbr->buildComparison( $op, [ 'cl_timestamp' => $dbr->timestamp( $minClTime ) ] )
236  );
237  }
238 
239  return $queryBuilder;
240  }
241 
247  protected function getTimestampOffset( $rand ) {
248  if ( $rand === false ) {
249  return false;
250  }
251  if ( !$this->minTimestamp || !$this->maxTimestamp ) {
252  $minAndMax = $this->getMinAndMaxForCat();
253  if ( $minAndMax === null ) {
254  // No entries in this category.
255  return false;
256  }
257  [ $this->minTimestamp, $this->maxTimestamp ] = $minAndMax;
258  }
259 
260  $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
261 
262  return intval( $ts );
263  }
264 
270  protected function getMinAndMaxForCat() {
271  $dbr = $this->dbProvider->getReplicaDatabase();
272  $res = $dbr->newSelectQueryBuilder()
273  ->select( [ 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ] )
274  ->from( 'categorylinks' )
275  ->where( [ 'cl_to' => $this->category->getDBkey(), ] )
276  ->caller( __METHOD__ )->fetchRow();
277  if ( !$res ) {
278  return null;
279  }
280 
281  return [ (int)wfTimestamp( TS_UNIX, $res->low ), (int)wfTimestamp( TS_UNIX, $res->high ) ];
282  }
283 
291  private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
292  return $this->getQueryBuilder( $rand, $offset, $up )->caller( $fname )->fetchRow();
293  }
294 
295  protected function getGroupName() {
296  return 'redirects';
297  }
298 }
299 
304 class_alias( SpecialRandomInCategory::class, 'SpecialRandomInCategory' );
const NS_CATEGORY
Definition: Defines.php:78
wfRandom()
Get a random decimal value in the domain of [0, 1), in a way not likely to give duplicate values for ...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:158
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
Definition: HTMLForm.php:1625
Special page which uses an HTMLForm to handle processing.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page to direct the user to a random page.
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
onSubmit(array $data)
Process the form on submission.
getMinAndMaxForCat()
Get the lowest and highest timestamp for a category.
requiresPost()
Whether this action should using POST method to submit, default to true.
getDisplayFormat()
Get display format for the form.
__construct(IConnectionProvider $dbProvider)
getFormFields()
Get an HTMLForm descriptor array.
getSubpageField()
Override this function to set the field name used in the subpage syntax.
setCategory(Title $cat)
Set which category to use.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:400
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:624
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
Build SELECT queries with a fluent interface.
Provide primary and replica IDatabase connections.