MediaWiki master
SpecialRandomInCategory.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
9use BadMethodCallException;
15use stdClass;
18use Wikimedia\Timestamp\TimestampFormat as TS;
19
47 protected $extra = [];
49 protected $category = false;
51 protected $maxOffset = 30;
53 private $maxTimestamp = null;
55 private $minTimestamp = null;
56
57 public function __construct(
58 private readonly IConnectionProvider $dbProvider,
59 ) {
60 parent::__construct( 'RandomInCategory' );
61 }
62
66 public function setCategory( Title $cat ) {
67 $this->category = $cat;
68 $this->maxTimestamp = null;
69 $this->minTimestamp = null;
70 }
71
73 protected function getFormFields() {
74 $this->addHelpLink( 'Help:RandomInCategory' );
75
76 return [
77 'category' => [
78 'type' => 'title',
79 'namespace' => NS_CATEGORY,
80 'relative' => true,
81 'label-message' => 'randomincategory-category',
82 'required' => true,
83 ]
84 ];
85 }
86
88 public function requiresPost() {
89 return false;
90 }
91
93 protected function getDisplayFormat() {
94 return 'ooui';
95 }
96
97 protected function alterForm( HTMLForm $form ) {
98 $form->setSubmitTextMsg( 'randomincategory-submit' );
99 }
100
102 protected function getSubpageField() {
103 return 'category';
104 }
105
107 public function onSubmit( array $data ) {
108 $cat = false;
109
110 $categoryStr = $data['category'];
111
112 if ( $categoryStr ) {
113 $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
114 }
115
116 if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
117 // Someone searching for something like "Wikipedia:Foo"
118 $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
119 }
120
121 if ( $cat ) {
122 $this->setCategory( $cat );
123 }
124
125 if ( !$this->category && $categoryStr ) {
126 $msg = $this->msg( 'randomincategory-invalidcategory',
127 wfEscapeWikiText( $categoryStr ) );
128
129 return Status::newFatal( $msg );
130
131 } elseif ( !$this->category ) {
132 return false; // no data sent
133 }
134
135 $title = $this->getRandomTitle();
136
137 if ( $title === null ) {
138 $msg = $this->msg( 'randomincategory-nopages',
139 $this->category->getText() );
140
141 return Status::newFatal( $msg );
142 }
143
144 $query = $this->getRequest()->getQueryValues();
145 unset( $query['title'] );
146 $this->getOutput()->redirect( $title->getFullURL( $query ) );
147 }
148
153 public function getRandomTitle() {
154 // Convert to float, since we do math with the random number.
155 $rand = (float)wfRandom();
156
157 // Given that timestamps are rather unevenly distributed, we also
158 // use an offset between 0 and 30 to make any biases less noticeable.
159 $offset = mt_rand( 0, $this->maxOffset );
160
161 if ( mt_rand( 0, 1 ) ) {
162 $up = true;
163 } else {
164 $up = false;
165 }
166
167 $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
168
169 // Try again without the timestamp offset (wrap around the end)
170 if ( !$row ) {
171 $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
172 }
173
174 // Maybe the category is really small and offset too high
175 if ( !$row ) {
176 $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
177 }
178
179 // Just get the first entry.
180 if ( !$row ) {
181 $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
182 }
183
184 if ( $row ) {
185 return Title::makeTitle( $row->page_namespace, $row->page_title );
186 }
187
188 return null;
189 }
190
202 protected function getQueryBuilder( $rand, $offset, $up ) {
203 if ( !$this->category instanceof Title ) {
204 throw new BadMethodCallException( 'No category set' );
205 }
206 $dbr = $this->dbProvider->getReplicaDatabase( CategoryLinksTable::VIRTUAL_DOMAIN );
207 $queryBuilder = $dbr->newSelectQueryBuilder()
208 ->select( [ 'page_title', 'page_namespace' ] )
209 ->from( 'categorylinks' )
210 ->join( 'linktarget', null, 'cl_target_id = lt_id' )
211 ->join( 'page', null, 'cl_from = page_id' )
212 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] )
213 ->andWhere( $this->extra )
214 ->orderBy( 'cl_timestamp', $up ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC )
215 ->limit( 1 )
216 ->offset( $offset );
217
218 $minClTime = $this->getTimestampOffset( $rand );
219 if ( $minClTime ) {
220 $op = $up ? '>=' : '<=';
221 $queryBuilder->andWhere(
222 $dbr->expr( 'cl_timestamp', $op, $dbr->timestamp( $minClTime ) )
223 );
224 }
225
226 return $queryBuilder;
227 }
228
233 protected function getTimestampOffset( $rand ) {
234 if ( $rand === false ) {
235 return false;
236 }
237 if ( !$this->minTimestamp || !$this->maxTimestamp ) {
238 $minAndMax = $this->getMinAndMaxForCat();
239 if ( $minAndMax === null ) {
240 // No entries in this category.
241 return false;
242 }
243 [ $this->minTimestamp, $this->maxTimestamp ] = $minAndMax;
244 }
245
246 $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
247
248 return intval( $ts );
249 }
250
256 protected function getMinAndMaxForCat() {
257 $dbr = $this->dbProvider->getReplicaDatabase( CategoryLinksTable::VIRTUAL_DOMAIN );
258 $res = $dbr->newSelectQueryBuilder()
259 ->select( [ 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ] )
260 ->from( 'categorylinks' )
261 ->join( 'linktarget', null, 'cl_target_id = lt_id' )
262 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] )
263 ->caller( __METHOD__ )
264 ->fetchRow();
265
266 if ( !$res ) {
267 return null;
268 }
269
270 return [ (int)wfTimestamp( TS::UNIX, $res->low ), (int)wfTimestamp( TS::UNIX, $res->high ) ];
271 }
272
280 private function selectRandomPageFromDB( $rand, $offset, $up, $fname ) {
281 return $this->getQueryBuilder( $rand, $offset, $up )->caller( $fname )->fetchRow();
282 }
283
285 protected function getGroupName() {
286 return 'redirects';
287 }
288}
289
294class_alias( SpecialRandomInCategory::class, 'SpecialRandomInCategory' );
const NS_CATEGORY
Definition Defines.php:65
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.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:207
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.bool|string|array|Status As documented for HTMLForm::trySubmit.
getMinAndMaxForCat()
Get the lowest and highest timestamp for a category.
requiresPost()
Whether this action should using POST method to submit, default to true.1.40 bool
getDisplayFormat()
Get display format for the form.See HTMLForm documentation for available values.1....
__construct(private readonly IConnectionProvider $dbProvider,)
getFormFields()
Get an HTMLForm descriptor array.array
getSubpageField()
Override this function to set the field name used in the subpage syntax.1.40 false|string
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:44
Represents a title within MediaWiki.
Definition Title.php:69
Build SELECT queries with a fluent interface.
Provide primary and replica IDatabase connections.