MediaWiki master
SpecialRandomInCategory.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use BadMethodCallException;
30use stdClass;
33
61 protected $extra = [];
63 protected $category = false;
65 protected $maxOffset = 30;
67 private $maxTimestamp = null;
69 private $minTimestamp = null;
70
71 private IConnectionProvider $dbProvider;
72
73 public function __construct( IConnectionProvider $dbProvider ) {
74 parent::__construct( 'RandomInCategory' );
75 $this->dbProvider = $dbProvider;
76 }
77
81 public function setCategory( Title $cat ) {
82 $this->category = $cat;
83 $this->maxTimestamp = null;
84 $this->minTimestamp = null;
85 }
86
87 protected function getFormFields() {
88 $this->addHelpLink( 'Help:RandomInCategory' );
89
90 return [
91 'category' => [
92 'type' => 'title',
93 'namespace' => NS_CATEGORY,
94 'relative' => true,
95 'label-message' => 'randomincategory-category',
96 'required' => true,
97 ]
98 ];
99 }
100
101 public function requiresPost() {
102 return false;
103 }
104
105 protected function getDisplayFormat() {
106 return 'ooui';
107 }
108
109 protected function alterForm( HTMLForm $form ) {
110 $form->setSubmitTextMsg( 'randomincategory-submit' );
111 }
112
113 protected function getSubpageField() {
114 return 'category';
115 }
116
117 public function onSubmit( array $data ) {
118 $cat = false;
119
120 $categoryStr = $data['category'];
121
122 if ( $categoryStr ) {
123 $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
124 }
125
126 if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
127 // Someone searching for something like "Wikipedia:Foo"
128 $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
129 }
130
131 if ( $cat ) {
132 $this->setCategory( $cat );
133 }
134
135 if ( !$this->category && $categoryStr ) {
136 $msg = $this->msg( 'randomincategory-invalidcategory',
137 wfEscapeWikiText( $categoryStr ) );
138
139 return Status::newFatal( $msg );
140
141 } elseif ( !$this->category ) {
142 return false; // no data sent
143 }
144
145 $title = $this->getRandomTitle();
146
147 if ( $title === null ) {
148 $msg = $this->msg( 'randomincategory-nopages',
149 $this->category->getText() );
150
151 return Status::newFatal( $msg );
152 }
153
154 $query = $this->getRequest()->getQueryValues();
155 unset( $query['title'] );
156 $this->getOutput()->redirect( $title->getFullURL( $query ) );
157 }
158
163 public function getRandomTitle() {
164 // Convert to float, since we do math with the random number.
165 $rand = (float)wfRandom();
166
167 // Given that timestamps are rather unevenly distributed, we also
168 // use an offset between 0 and 30 to make any biases less noticeable.
169 $offset = mt_rand( 0, $this->maxOffset );
170
171 if ( mt_rand( 0, 1 ) ) {
172 $up = true;
173 } else {
174 $up = false;
175 }
176
177 $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
178
179 // Try again without the timestamp offset (wrap around the end)
180 if ( !$row ) {
181 $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
182 }
183
184 // Maybe the category is really small and offset too high
185 if ( !$row ) {
186 $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
187 }
188
189 // Just get the first entry.
190 if ( !$row ) {
191 $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
192 }
193
194 if ( $row ) {
195 return Title::makeTitle( $row->page_namespace, $row->page_title );
196 }
197
198 return null;
199 }
200
212 protected function getQueryBuilder( $rand, $offset, $up ) {
213 if ( !$this->category instanceof Title ) {
214 throw new BadMethodCallException( 'No category set' );
215 }
216 $dbr = $this->dbProvider->getReplicaDatabase();
217 $categoryLinksMigrationStage = MediaWikiServices::getInstance()->getMainConfig()->get(
219 );
220 $queryBuilder = $dbr->newSelectQueryBuilder()
221 ->select( [ 'page_title', 'page_namespace' ] )
222 ->from( 'categorylinks' )
223 ->join( 'page', null, 'cl_from = page_id' )
224
225 ->andWhere( $this->extra )
226 ->orderBy( 'cl_timestamp', $up ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC )
227 ->limit( 1 )
228 ->offset( $offset );
229 if ( $categoryLinksMigrationStage & SCHEMA_COMPAT_READ_OLD ) {
230 $queryBuilder->where( [ 'cl_to' => $this->category->getDBkey() ] );
231 } else {
232 $queryBuilder->join( 'linktarget', null, 'cl_target_id = lt_id' )
233 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] );
234 }
235
236 $minClTime = $this->getTimestampOffset( $rand );
237 if ( $minClTime ) {
238 $op = $up ? '>=' : '<=';
239 $queryBuilder->andWhere(
240 $dbr->expr( 'cl_timestamp', $op, $dbr->timestamp( $minClTime ) )
241 );
242 }
243
244 return $queryBuilder;
245 }
246
251 protected function getTimestampOffset( $rand ) {
252 if ( $rand === false ) {
253 return false;
254 }
255 if ( !$this->minTimestamp || !$this->maxTimestamp ) {
256 $minAndMax = $this->getMinAndMaxForCat();
257 if ( $minAndMax === null ) {
258 // No entries in this category.
259 return false;
260 }
261 [ $this->minTimestamp, $this->maxTimestamp ] = $minAndMax;
262 }
263
264 $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
265
266 return intval( $ts );
267 }
268
274 protected function getMinAndMaxForCat() {
275 $dbr = $this->dbProvider->getReplicaDatabase();
276 $categoryLinksMigrationStage = MediaWikiServices::getInstance()->getMainConfig()->get(
278 );
279 $queryBuilder = $dbr->newSelectQueryBuilder()
280 ->select( [ 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ] )
281 ->from( 'categorylinks' );
282 if ( $categoryLinksMigrationStage & SCHEMA_COMPAT_READ_OLD ) {
283 $queryBuilder->where( [ 'cl_to' => $this->category->getDBkey(), ] );
284 } else {
285 $queryBuilder->join( 'linktarget', null, 'cl_target_id = lt_id' )
286 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] );
287 }
288 $res = $queryBuilder->caller( __METHOD__ )->fetchRow();
289 if ( !$res ) {
290 return null;
291 }
292
293 return [ (int)wfTimestamp( TS_UNIX, $res->low ), (int)wfTimestamp( TS_UNIX, $res->high ) ];
294 }
295
303 private function selectRandomPageFromDB( $rand, $offset, $up, $fname ) {
304 return $this->getQueryBuilder( $rand, $offset, $up )->caller( $fname )->fetchRow();
305 }
306
307 protected function getGroupName() {
308 return 'redirects';
309 }
310}
311
316class_alias( SpecialRandomInCategory::class, 'SpecialRandomInCategory' );
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:283
const NS_CATEGORY
Definition Defines.php:79
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:209
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
A class containing constants representing the names of configuration variables.
const CategoryLinksSchemaMigrationStage
Name constant for the CategoryLinksSchemaMigrationStage setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
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.