MediaWiki REL1_37
SpecialRandomInCategory.php
Go to the documentation of this file.
1<?php
26
53 protected $extra = []; // Extra SQL statements
55 protected $category = false; // Title object of category
57 protected $maxOffset = 30; // Max amount to fudge randomness by.
59 private $maxTimestamp = null;
61 private $minTimestamp = null;
62
65
70 parent::__construct( 'RandomInCategory' );
71 $this->loadBalancer = $loadBalancer;
72 }
73
78 public function setCategory( Title $cat ) {
79 $this->category = $cat;
80 $this->maxTimestamp = null;
81 $this->minTimestamp = null;
82 }
83
84 protected function getFormFields() {
85 $this->addHelpLink( 'Help:RandomInCategory' );
86
87 return [
88 'category' => [
89 'type' => 'title',
90 'namespace' => NS_CATEGORY,
91 'relative' => true,
92 'label-message' => 'randomincategory-category',
93 'required' => true,
94 ]
95 ];
96 }
97
98 public function requiresWrite() {
99 return false;
100 }
101
102 public function requiresUnblock() {
103 return false;
104 }
105
106 protected function getDisplayFormat() {
107 return 'ooui';
108 }
109
110 protected function alterForm( HTMLForm $form ) {
111 $form->setSubmitTextMsg( 'randomincategory-submit' );
112 }
113
114 protected function setParameter( $par ) {
115 // if subpage present, fake form submission
116 $this->onSubmit( [ 'category' => $par ] );
117 }
118
119 public function onSubmit( array $data ) {
120 $cat = false;
121
122 $categoryStr = $data['category'];
123
124 if ( $categoryStr ) {
125 $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
126 }
127
128 if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
129 // Someone searching for something like "Wikipedia:Foo"
130 $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
131 }
132
133 if ( $cat ) {
134 $this->setCategory( $cat );
135 }
136
137 if ( !$this->category && $categoryStr ) {
138 $msg = $this->msg( 'randomincategory-invalidcategory',
139 wfEscapeWikiText( $categoryStr ) );
140
141 return Status::newFatal( $msg );
142
143 } elseif ( !$this->category ) {
144 return false; // no data sent
145 }
146
147 $title = $this->getRandomTitle();
148
149 if ( $title === null ) {
150 $msg = $this->msg( 'randomincategory-nopages',
151 $this->category->getText() );
152
153 return Status::newFatal( $msg );
154 }
155
156 $query = $this->getRequest()->getValues();
157 unset( $query['title'] );
158 $this->getOutput()->redirect( $title->getFullURL( $query ) );
159 }
160
165 public function getRandomTitle() {
166 // Convert to float, since we do math with the random number.
167 $rand = (float)wfRandom();
168 $title = null;
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 ( Exception $e ) {
260 // Possibly no entries in category.
261 return false;
262 }
263 }
264
265 $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
266
267 return intval( $ts );
268 }
269
277 protected function getMinAndMaxForCat( Title $category ) {
278 $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
279 $res = $dbr->selectRow(
280 'categorylinks',
281 [
282 'low' => 'MIN( cl_timestamp )',
283 'high' => 'MAX( cl_timestamp )'
284 ],
285 [
286 'cl_to' => $this->category->getDBkey(),
287 ],
288 __METHOD__
289 );
290 if ( !$res ) {
291 throw new MWException( 'No entries in category' );
292 }
293
294 return [ wfTimestamp( TS_UNIX, $res->low ), wfTimestamp( TS_UNIX, $res->high ) ];
295 }
296
304 private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
305 $dbr = $this->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
306
307 $query = $this->getQueryInfo( $rand, $offset, $up );
308 $res = $dbr->select(
309 $query['tables'],
310 $query['fields'],
311 $query['conds'],
312 $fname,
313 $query['options'],
314 $query['join_conds']
315 );
316
317 return $res->fetchObject();
318 }
319
320 protected function getGroupName() {
321 return 'redirects';
322 }
323}
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:143
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)
selectRandomPageFromDB( $rand, $offset, $up, $fname=__METHOD__)
getFormFields()
Get an HTMLForm descriptor array.
__construct(ILoadBalancer $loadBalancer)
Represents a title within MediaWiki.
Definition Title.php:48
Database cluster connection, tracking, load balancing, and transaction manager interface.