MediaWiki REL1_39
SpecialRandomInCategory.php
Go to the documentation of this file.
1<?php
26use 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
70 public function __construct( ILoadBalancer $loadBalancer ) {
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()->getQueryValues();
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.
MediaWiki exception.
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)
getFormFields()
Get an HTMLForm descriptor array.
__construct(ILoadBalancer $loadBalancer)
Represents a title within MediaWiki.
Definition Title.php:49
Create and track the database connections and transactions for a given database cluster.