MediaWiki  master
SpecialRandomInCategory.php
Go to the documentation of this file.
1 <?php
26 use Wikimedia\RequestTimeout\TimeoutException;
27 
54  protected $extra = []; // Extra SQL statements
56  protected $category = false; // Title object of category
58  protected $maxOffset = 30; // Max amount to fudge randomness by.
60  private $maxTimestamp = null;
62  private $minTimestamp = null;
63 
65  private $loadBalancer;
66 
71  parent::__construct( 'RandomInCategory' );
72  $this->loadBalancer = $loadBalancer;
73  }
74 
79  public function setCategory( Title $cat ) {
80  $this->category = $cat;
81  $this->maxTimestamp = null;
82  $this->minTimestamp = null;
83  }
84 
85  protected function getFormFields() {
86  $this->addHelpLink( 'Help:RandomInCategory' );
87 
88  return [
89  'category' => [
90  'type' => 'title',
91  'namespace' => NS_CATEGORY,
92  'relative' => true,
93  'label-message' => 'randomincategory-category',
94  'required' => true,
95  ]
96  ];
97  }
98 
99  public function requiresWrite() {
100  return false;
101  }
102 
103  public function requiresUnblock() {
104  return false;
105  }
106 
107  protected function getDisplayFormat() {
108  return 'ooui';
109  }
110 
111  protected function alterForm( HTMLForm $form ) {
112  $form->setSubmitTextMsg( 'randomincategory-submit' );
113  }
114 
115  protected function setParameter( $par ) {
116  // if subpage present, fake form submission
117  $this->onSubmit( [ 'category' => $par ] );
118  }
119 
120  public function onSubmit( array $data ) {
121  $cat = false;
122 
123  $categoryStr = $data['category'];
124 
125  if ( $categoryStr ) {
126  $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
127  }
128 
129  if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
130  // Someone searching for something like "Wikipedia:Foo"
131  $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
132  }
133 
134  if ( $cat ) {
135  $this->setCategory( $cat );
136  }
137 
138  if ( !$this->category && $categoryStr ) {
139  $msg = $this->msg( 'randomincategory-invalidcategory',
140  wfEscapeWikiText( $categoryStr ) );
141 
142  return Status::newFatal( $msg );
143 
144  } elseif ( !$this->category ) {
145  return false; // no data sent
146  }
147 
148  $title = $this->getRandomTitle();
149 
150  if ( $title === null ) {
151  $msg = $this->msg( 'randomincategory-nopages',
152  $this->category->getText() );
153 
154  return Status::newFatal( $msg );
155  }
156 
157  $query = $this->getRequest()->getValues();
158  unset( $query['title'] );
159  $this->getOutput()->redirect( $title->getFullURL( $query ) );
160  }
161 
166  public function getRandomTitle() {
167  // Convert to float, since we do math with the random number.
168  $rand = (float)wfRandom();
169 
170  // Given that timestamps are rather unevenly distributed, we also
171  // use an offset between 0 and 30 to make any biases less noticeable.
172  $offset = mt_rand( 0, $this->maxOffset );
173 
174  if ( mt_rand( 0, 1 ) ) {
175  $up = true;
176  } else {
177  $up = false;
178  }
179 
180  $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
181 
182  // Try again without the timestamp offset (wrap around the end)
183  if ( !$row ) {
184  $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
185  }
186 
187  // Maybe the category is really small and offset too high
188  if ( !$row ) {
189  $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
190  }
191 
192  // Just get the first entry.
193  if ( !$row ) {
194  $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
195  }
196 
197  if ( $row ) {
198  return Title::makeTitle( $row->page_namespace, $row->page_title );
199  }
200 
201  return null;
202  }
203 
215  protected function getQueryInfo( $rand, $offset, $up ) {
216  $op = $up ? '>=' : '<=';
217  $dir = $up ? 'ASC' : 'DESC';
218  if ( !$this->category instanceof Title ) {
219  throw new MWException( 'No category set' );
220  }
221  $qi = [
222  'tables' => [ 'categorylinks', 'page' ],
223  'fields' => [ 'page_title', 'page_namespace' ],
224  'conds' => array_merge( [
225  'cl_to' => $this->category->getDBkey(),
226  ], $this->extra ),
227  'options' => [
228  'ORDER BY' => 'cl_timestamp ' . $dir,
229  'LIMIT' => 1,
230  'OFFSET' => $offset
231  ],
232  'join_conds' => [
233  'page' => [ 'JOIN', 'cl_from = page_id' ]
234  ]
235  ];
236 
237  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
238  $minClTime = $this->getTimestampOffset( $rand );
239  if ( $minClTime ) {
240  $qi['conds'][] = 'cl_timestamp ' . $op . ' ' .
241  $dbr->addQuotes( $dbr->timestamp( $minClTime ) );
242  }
243 
244  return $qi;
245  }
246 
252  protected function getTimestampOffset( $rand ) {
253  if ( $rand === false ) {
254  return false;
255  }
256  if ( !$this->minTimestamp || !$this->maxTimestamp ) {
257  try {
258  list( $this->minTimestamp, $this->maxTimestamp ) = $this->getMinAndMaxForCat( $this->category );
259  } catch ( TimeoutException $e ) {
260  throw $e;
261  } catch ( Exception $e ) {
262  // Possibly no entries in category.
263  return false;
264  }
265  }
266 
267  $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
268 
269  return intval( $ts );
270  }
271 
279  protected function getMinAndMaxForCat( Title $category ) {
280  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
281  $res = $dbr->selectRow(
282  'categorylinks',
283  [
284  'low' => 'MIN( cl_timestamp )',
285  'high' => 'MAX( cl_timestamp )'
286  ],
287  [
288  'cl_to' => $this->category->getDBkey(),
289  ],
290  __METHOD__
291  );
292  if ( !$res ) {
293  throw new MWException( 'No entries in category' );
294  }
295 
296  return [ (int)wfTimestamp( TS_UNIX, $res->low ), (int)wfTimestamp( TS_UNIX, $res->high ) ];
297  }
298 
306  private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
307  $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
308 
309  $query = $this->getQueryInfo( $rand, $offset, $up );
310  $res = $dbr->select(
311  $query['tables'],
312  $query['fields'],
313  $query['conds'],
314  $fname,
315  $query['options'],
316  $query['join_conds']
317  );
318 
319  return $res->fetchObject();
320  }
321 
322  protected function getGroupName() {
323  return 'redirects';
324  }
325 }
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,...
Special page which uses an HTMLForm to handle processing.
string null $par
The sub-page of the special page.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:150
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
Definition: HTMLForm.php:1578
MediaWiki exception.
Definition: MWException.php:29
getOutput()
Get the OutputPage being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest 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.
setParameter( $par)
Maybe do something interesting with the subpage parameter.
requiresWrite()
Whether this action requires the wiki not to be locked.
getDisplayFormat()
Get display format for the form.
requiresUnblock()
Whether this action cannot be executed by a blocked user.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getRandomTitle()
Choose a random title.
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
onSubmit(array $data)
Process the form on POST submission.
getMinAndMaxForCat(Title $category)
Get the lowest and highest timestamp for a category.
setCategory(Title $cat)
Set which category to use.
getQueryInfo( $rand, $offset, $up)
selectRandomPageFromDB( $rand, $offset, $up, $fname=__METHOD__)
getFormFields()
Get an HTMLForm descriptor array.
__construct(ILoadBalancer $loadBalancer)
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
Represents a title within MediaWiki.
Definition: Title.php:49
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition: defines.php:25