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 private IConnectionProvider $dbProvider;
58
59 public function __construct( IConnectionProvider $dbProvider ) {
60 parent::__construct( 'RandomInCategory' );
61 $this->dbProvider = $dbProvider;
62 }
63
67 public function setCategory( Title $cat ) {
68 $this->category = $cat;
69 $this->maxTimestamp = null;
70 $this->minTimestamp = null;
71 }
72
74 protected function getFormFields() {
75 $this->addHelpLink( 'Help:RandomInCategory' );
76
77 return [
78 'category' => [
79 'type' => 'title',
80 'namespace' => NS_CATEGORY,
81 'relative' => true,
82 'label-message' => 'randomincategory-category',
83 'required' => true,
84 ]
85 ];
86 }
87
89 public function requiresPost() {
90 return false;
91 }
92
94 protected function getDisplayFormat() {
95 return 'ooui';
96 }
97
98 protected function alterForm( HTMLForm $form ) {
99 $form->setSubmitTextMsg( 'randomincategory-submit' );
100 }
101
103 protected function getSubpageField() {
104 return 'category';
105 }
106
108 public function onSubmit( array $data ) {
109 $cat = false;
110
111 $categoryStr = $data['category'];
112
113 if ( $categoryStr ) {
114 $cat = Title::newFromText( $categoryStr, NS_CATEGORY );
115 }
116
117 if ( $cat && $cat->getNamespace() !== NS_CATEGORY ) {
118 // Someone searching for something like "Wikipedia:Foo"
119 $cat = Title::makeTitleSafe( NS_CATEGORY, $categoryStr );
120 }
121
122 if ( $cat ) {
123 $this->setCategory( $cat );
124 }
125
126 if ( !$this->category && $categoryStr ) {
127 $msg = $this->msg( 'randomincategory-invalidcategory',
128 wfEscapeWikiText( $categoryStr ) );
129
130 return Status::newFatal( $msg );
131
132 } elseif ( !$this->category ) {
133 return false; // no data sent
134 }
135
136 $title = $this->getRandomTitle();
137
138 if ( $title === null ) {
139 $msg = $this->msg( 'randomincategory-nopages',
140 $this->category->getText() );
141
142 return Status::newFatal( $msg );
143 }
144
145 $query = $this->getRequest()->getQueryValues();
146 unset( $query['title'] );
147 $this->getOutput()->redirect( $title->getFullURL( $query ) );
148 }
149
154 public function getRandomTitle() {
155 // Convert to float, since we do math with the random number.
156 $rand = (float)wfRandom();
157
158 // Given that timestamps are rather unevenly distributed, we also
159 // use an offset between 0 and 30 to make any biases less noticeable.
160 $offset = mt_rand( 0, $this->maxOffset );
161
162 if ( mt_rand( 0, 1 ) ) {
163 $up = true;
164 } else {
165 $up = false;
166 }
167
168 $row = $this->selectRandomPageFromDB( $rand, $offset, $up, __METHOD__ );
169
170 // Try again without the timestamp offset (wrap around the end)
171 if ( !$row ) {
172 $row = $this->selectRandomPageFromDB( false, $offset, $up, __METHOD__ );
173 }
174
175 // Maybe the category is really small and offset too high
176 if ( !$row ) {
177 $row = $this->selectRandomPageFromDB( $rand, 0, $up, __METHOD__ );
178 }
179
180 // Just get the first entry.
181 if ( !$row ) {
182 $row = $this->selectRandomPageFromDB( false, 0, true, __METHOD__ );
183 }
184
185 if ( $row ) {
186 return Title::makeTitle( $row->page_namespace, $row->page_title );
187 }
188
189 return null;
190 }
191
203 protected function getQueryBuilder( $rand, $offset, $up ) {
204 if ( !$this->category instanceof Title ) {
205 throw new BadMethodCallException( 'No category set' );
206 }
207 $dbr = $this->dbProvider->getReplicaDatabase( CategoryLinksTable::VIRTUAL_DOMAIN );
208 $queryBuilder = $dbr->newSelectQueryBuilder()
209 ->select( [ 'page_title', 'page_namespace' ] )
210 ->from( 'categorylinks' )
211 ->join( 'linktarget', null, 'cl_target_id = lt_id' )
212 ->join( 'page', null, 'cl_from = page_id' )
213 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] )
214 ->andWhere( $this->extra )
215 ->orderBy( 'cl_timestamp', $up ? SelectQueryBuilder::SORT_ASC : SelectQueryBuilder::SORT_DESC )
216 ->limit( 1 )
217 ->offset( $offset );
218
219 $minClTime = $this->getTimestampOffset( $rand );
220 if ( $minClTime ) {
221 $op = $up ? '>=' : '<=';
222 $queryBuilder->andWhere(
223 $dbr->expr( 'cl_timestamp', $op, $dbr->timestamp( $minClTime ) )
224 );
225 }
226
227 return $queryBuilder;
228 }
229
234 protected function getTimestampOffset( $rand ) {
235 if ( $rand === false ) {
236 return false;
237 }
238 if ( !$this->minTimestamp || !$this->maxTimestamp ) {
239 $minAndMax = $this->getMinAndMaxForCat();
240 if ( $minAndMax === null ) {
241 // No entries in this category.
242 return false;
243 }
244 [ $this->minTimestamp, $this->maxTimestamp ] = $minAndMax;
245 }
246
247 $ts = ( $this->maxTimestamp - $this->minTimestamp ) * $rand + $this->minTimestamp;
248
249 return intval( $ts );
250 }
251
257 protected function getMinAndMaxForCat() {
258 $dbr = $this->dbProvider->getReplicaDatabase( CategoryLinksTable::VIRTUAL_DOMAIN );
259 $res = $dbr->newSelectQueryBuilder()
260 ->select( [ 'low' => 'MIN( cl_timestamp )', 'high' => 'MAX( cl_timestamp )' ] )
261 ->from( 'categorylinks' )
262 ->join( 'linktarget', null, 'cl_target_id = lt_id' )
263 ->where( [ 'lt_title' => $this->category->getDBkey(), 'lt_namespace' => NS_CATEGORY ] )
264 ->caller( __METHOD__ )
265 ->fetchRow();
266
267 if ( !$res ) {
268 return null;
269 }
270
271 return [ (int)wfTimestamp( TS::UNIX, $res->low ), (int)wfTimestamp( TS::UNIX, $res->high ) ];
272 }
273
281 private function selectRandomPageFromDB( $rand, $offset, $up, $fname ) {
282 return $this->getQueryBuilder( $rand, $offset, $up )->caller( $fname )->fetchRow();
283 }
284
286 protected function getGroupName() {
287 return 'redirects';
288 }
289}
290
295class_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:195
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....
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.