MediaWiki master
SpecialRandomInCategory.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use BadMethodCallException;
28use stdClass;
31
59 protected $extra = [];
61 protected $category = false;
63 protected $maxOffset = 30;
65 private $maxTimestamp = null;
67 private $minTimestamp = null;
68
69 private IConnectionProvider $dbProvider;
70
74 public function __construct( IConnectionProvider $dbProvider ) {
75 parent::__construct( 'RandomInCategory' );
76 $this->dbProvider = $dbProvider;
77 }
78
83 public function setCategory( Title $cat ) {
84 $this->category = $cat;
85 $this->maxTimestamp = null;
86 $this->minTimestamp = null;
87 }
88
89 protected function getFormFields() {
90 $this->addHelpLink( 'Help:RandomInCategory' );
91
92 return [
93 'category' => [
94 'type' => 'title',
95 'namespace' => NS_CATEGORY,
96 'relative' => true,
97 'label-message' => 'randomincategory-category',
98 'required' => true,
99 ]
100 ];
101 }
102
103 public function requiresPost() {
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 getSubpageField() {
116 return 'category';
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()->getQueryValues();
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
169 // Given that timestamps are rather unevenly distributed, we also
170 // use an offset between 0 and 30 to make any biases less noticeable.
171 $offset = mt_rand( 0, $this->maxOffset );
172
173 if ( mt_rand( 0, 1 ) ) {
174 $up = true;
175 } else {
176 $up = false;
177 }
178
179 $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
180
181 // Try again without the timestamp offset (wrap around the end)
182 if ( !$row ) {
183 $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
184 }
185
186 // Maybe the category is really small and offset too high
187 if ( !$row ) {
188 $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
189 }
190
191 // Just get the first entry.
192 if ( !$row ) {
193 $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
194 }
195
196 if ( $row ) {
197 return Title::makeTitle( $row->page_namespace, $row->page_title );
198 }
199
200 return null;
201 }
202
214 protected function getQueryBuilder( $rand, $offset, $up ) {
215 if ( !$this->category instanceof Title ) {
216 throw new BadMethodCallException( 'No category set' );
217 }
218 $dbr = $this->dbProvider->getReplicaDatabase();
219 $queryBuilder = $dbr->newSelectQueryBuilder()
220 ->select( [ 'page_title', 'page_namespace' ] )
221 ->from( 'categorylinks' )
222 ->join( 'page', null, 'cl_from = page_id' )
223 ->where( [ 'cl_to' => $this->category->getDBkey() ] )
224 ->andWhere( $this->extra )
225 ->orderBy( 'cl_timestamp', $up ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC )
226 ->limit( 1 )
227 ->offset( $offset );
228
229 $minClTime = $this->getTimestampOffset( $rand );
230 if ( $minClTime ) {
231 $op = $up ? '>=' : '<=';
232 $queryBuilder->andWhere(
233 $dbr->expr( 'cl_timestamp', $op, $dbr->timestamp( $minClTime ) )
234 );
235 }
236
237 return $queryBuilder;
238 }
239
244 protected function getTimestampOffset( $rand ) {
245 if ( $rand === false ) {
246 return false;
247 }
248 if ( !$this->minTimestamp || !$this->maxTimestamp ) {
249 $minAndMax = $this->getMinAndMaxForCat();
250 if ( $minAndMax === null ) {
251 // No entries in this category.
252 return false;
253 }
254 [ $this->minTimestamp, $this->maxTimestamp ] = $minAndMax;
255 }
256
257 $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
258
259 return intval( $ts );
260 }
261
267 protected function getMinAndMaxForCat() {
268 $dbr = $this->dbProvider->getReplicaDatabase();
269 $res = $dbr->newSelectQueryBuilder()
270 ->select( [ 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ] )
271 ->from( 'categorylinks' )
272 ->where( [ 'cl_to' => $this->category->getDBkey(), ] )
273 ->caller( __METHOD__ )->fetchRow();
274 if ( !$res ) {
275 return null;
276 }
277
278 return [ (int)wfTimestamp( TS_UNIX, $res->low ), (int)wfTimestamp( TS_UNIX, $res->high ) ];
279 }
280
288 private function selectRandomPageFromDB( $rand, $offset, $up, $fname = __METHOD__ ) {
289 return $this->getQueryBuilder( $rand, $offset, $up )->caller( $fname )->fetchRow();
290 }
291
292 protected function getGroupName() {
293 return 'redirects';
294 }
295}
296
301class_alias( SpecialRandomInCategory::class, 'SpecialRandomInCategory' );
getQueryBuilder()
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 ...
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:206
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
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.
Redirect to a random page in a category.
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.
getFormFields()
Get an HTMLForm descriptor array.
getSubpageField()
Override this function to set the field name used in the subpage syntax.
int $maxOffset
Max amount to fudge randomness by.
Title false $category
Title object of category.
setCategory(Title $cat)
Set which category to use.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
Build SELECT queries with a fluent interface.
Provide primary and replica IDatabase connections.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...