Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.12% |
912 / 1059 |
|
62.26% |
33 / 53 |
CRAP | |
0.00% |
0 / 1 |
ChangesListSpecialPage | |
86.20% |
912 / 1058 |
|
62.26% |
33 / 53 |
269.05 | |
0.00% |
0 / 1 |
__construct | |
96.40% |
428 / 444 |
|
0.00% |
0 / 1 |
7 | |||
removeRegistrationFilterDefinitions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
areFiltersInConflict | |
62.50% |
10 / 16 |
|
0.00% |
0 / 1 |
13.27 | |||
execute | |
52.94% |
27 / 51 |
|
0.00% |
0 / 1 |
42.68 | |||
setTempUserConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
considerActionsForDefaultSavedQuery | |
39.39% |
13 / 33 |
|
0.00% |
0 / 1 |
27.03 | |||
getLinkDays | |
45.45% |
5 / 11 |
|
0.00% |
0 / 1 |
6.60 | |||
includeRcFiltersApp | |
95.24% |
40 / 42 |
|
0.00% |
0 / 1 |
5 | |||
getRcFiltersConfigSummary | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getRcFiltersConfigVars | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
outputNoResults | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
outputTimeout | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getRows | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getOptions | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
registerFilters | |
100.00% |
57 / 57 |
|
100.00% |
1 / 1 |
7 | |||
transformFilterDefinition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
registerFiltersFromDefinitions | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
getLegacyShowHideFilters | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
setup | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getDefaultOptions | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
registerFilterGroup | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getFilterGroups | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFilterGroup | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStructuredFilterJsData | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
3 | |||
fetchOptionsFromRequest | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseParameters | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
10 | |||
validateOptions | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
fixContradictoryOptions | |
94.44% |
17 / 18 |
|
0.00% |
0 / 1 |
7.01 | |||
fixBackwardsCompatibilityOptions | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
replaceOldOptions | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
7 | |||
convertParamsForLink | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
buildQuery | |
96.43% |
27 / 28 |
|
0.00% |
0 / 1 |
8 | |||
doMainQuery | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
runMainQueryHook | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDB | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
webOutputHeader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
webOutput | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
outputFeedLinks | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
outputChangesList | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
doHeader | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setTopText | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setBottomText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getExtraOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeLegend | |
67.80% |
40 / 59 |
|
0.00% |
0 / 1 |
10.14 | |||
addModules | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRegisteredExpr | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getExperienceExpr | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
3 | |||
filterOnUserExperienceLevel | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
11 | |||
isStructuredFilterUiEnabled | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
checkStructuredFilterUiEnabled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultLimit | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultDays | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getLimitPreferenceName | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getSavedQueriesPreferenceName | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getDefaultDaysPreferenceName | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getCollapsedPreferenceName | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
expandSymbolicNamespaceFilters | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\SpecialPage; |
22 | |
23 | use ChangesListBooleanFilter; |
24 | use ChangesListBooleanFilterGroup; |
25 | use ChangesListFilterGroup; |
26 | use ChangesListStringOptionsFilterGroup; |
27 | use ChangeTags; |
28 | use MediaWiki\Context\IContextSource; |
29 | use MediaWiki\Html\FormOptions; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\Json\FormatJson; |
32 | use MediaWiki\MainConfigNames; |
33 | use MediaWiki\MediaWikiServices; |
34 | use MediaWiki\Parser\Sanitizer; |
35 | use MediaWiki\ResourceLoader as RL; |
36 | use MediaWiki\User\TempUser\TempUserConfig; |
37 | use MediaWiki\User\UserArray; |
38 | use MediaWiki\User\UserIdentity; |
39 | use MediaWiki\User\UserIdentityUtils; |
40 | use MWExceptionHandler; |
41 | use OOUI\IconWidget; |
42 | use RecentChange; |
43 | use Wikimedia\Rdbms\DBQueryTimeoutError; |
44 | use Wikimedia\Rdbms\FakeResultWrapper; |
45 | use Wikimedia\Rdbms\IExpression; |
46 | use Wikimedia\Rdbms\IReadableDatabase; |
47 | use Wikimedia\Rdbms\IResultWrapper; |
48 | use Wikimedia\Rdbms\RawSQLValue; |
49 | use Wikimedia\Timestamp\ConvertibleTimestamp; |
50 | |
51 | /** |
52 | * Special page which uses a ChangesList to show query results. |
53 | * |
54 | * @todo Most of the functions here should be protected instead of public. |
55 | * |
56 | * @ingroup RecentChanges |
57 | * @ingroup SpecialPage |
58 | */ |
59 | abstract class ChangesListSpecialPage extends SpecialPage { |
60 | |
61 | /** @var string */ |
62 | protected $rcSubpage; |
63 | |
64 | /** @var FormOptions */ |
65 | protected $rcOptions; |
66 | |
67 | protected UserIdentityUtils $userIdentityUtils; |
68 | protected TempUserConfig $tempUserConfig; |
69 | |
70 | // Order of both groups and filters is significant; first is top-most priority, |
71 | // descending from there. |
72 | // 'showHideSuffix' is a shortcut to and avoid spelling out |
73 | // details specific to subclasses here. |
74 | /** |
75 | * Definition information for the filters and their groups |
76 | * |
77 | * The value is $groupDefinition, a parameter to the ChangesListFilterGroup constructor. |
78 | * However, priority is dynamically added for the core groups, to ease maintenance. |
79 | * |
80 | * Groups are displayed to the user in the structured UI. However, if necessary, |
81 | * all of the filters in a group can be configured to only display on the |
82 | * unstuctured UI, in which case you don't need a group title. |
83 | * |
84 | * @var array |
85 | */ |
86 | private $filterGroupDefinitions; |
87 | |
88 | /** |
89 | * @var array Same format as filterGroupDefinitions, but for a single group (reviewStatus) |
90 | * that is registered conditionally. |
91 | */ |
92 | private $legacyReviewStatusFilterGroupDefinition; |
93 | |
94 | /** @var array Single filter group registered conditionally */ |
95 | private $reviewStatusFilterGroupDefinition; |
96 | |
97 | /** @var array Single filter group registered conditionally */ |
98 | private $hideCategorizationFilterDefinition; |
99 | |
100 | /** |
101 | * Filter groups, and their contained filters |
102 | * This is an associative array (with group name as key) of ChangesListFilterGroup objects. |
103 | * |
104 | * @var ChangesListFilterGroup[] |
105 | */ |
106 | protected $filterGroups = []; |
107 | |
108 | /** |
109 | * @param string $name |
110 | * @param string $restriction |
111 | * @param UserIdentityUtils $userIdentityUtils |
112 | * @param TempUserConfig $tempUserConfig |
113 | */ |
114 | public function __construct( |
115 | $name, |
116 | $restriction, |
117 | UserIdentityUtils $userIdentityUtils, |
118 | TempUserConfig $tempUserConfig |
119 | ) { |
120 | parent::__construct( $name, $restriction ); |
121 | |
122 | $this->userIdentityUtils = $userIdentityUtils; |
123 | $this->tempUserConfig = $tempUserConfig; |
124 | |
125 | $nonRevisionTypes = [ RC_LOG ]; |
126 | $this->getHookRunner()->onSpecialWatchlistGetNonRevisionTypes( $nonRevisionTypes ); |
127 | |
128 | $this->filterGroupDefinitions = [ |
129 | [ |
130 | 'name' => 'registration', |
131 | 'title' => 'rcfilters-filtergroup-registration', |
132 | 'class' => ChangesListBooleanFilterGroup::class, |
133 | 'filters' => [ |
134 | [ |
135 | 'name' => 'hideliu', |
136 | // rcshowhideliu-show, rcshowhideliu-hide, |
137 | // wlshowhideliu |
138 | 'showHideSuffix' => 'showhideliu', |
139 | 'default' => false, |
140 | 'queryCallable' => function ( string $specialClassName, IContextSource $ctx, |
141 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
142 | ) { |
143 | $conds[] = $this->getRegisteredExpr( false, $dbr ); |
144 | $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; |
145 | }, |
146 | 'isReplacedInStructuredUi' => true, |
147 | |
148 | ], |
149 | [ |
150 | 'name' => 'hideanons', |
151 | // rcshowhideanons-show, rcshowhideanons-hide, |
152 | // wlshowhideanons |
153 | 'showHideSuffix' => 'showhideanons', |
154 | 'default' => false, |
155 | 'queryCallable' => function ( string $specialClassName, IContextSource $ctx, |
156 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
157 | ) { |
158 | $conds[] = $this->getRegisteredExpr( true, $dbr ); |
159 | $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; |
160 | }, |
161 | 'isReplacedInStructuredUi' => true, |
162 | ] |
163 | ], |
164 | ], |
165 | |
166 | [ |
167 | 'name' => 'userExpLevel', |
168 | 'title' => 'rcfilters-filtergroup-user-experience-level', |
169 | 'class' => ChangesListStringOptionsFilterGroup::class, |
170 | 'isFullCoverage' => true, |
171 | 'filters' => [ |
172 | [ |
173 | 'name' => 'unregistered', |
174 | 'label' => 'rcfilters-filter-user-experience-level-unregistered-label', |
175 | 'description' => $this->tempUserConfig->isKnown() ? |
176 | 'rcfilters-filter-user-experience-level-unregistered-description-temp' : |
177 | 'rcfilters-filter-user-experience-level-unregistered-description', |
178 | 'cssClassSuffix' => 'user-unregistered', |
179 | 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { |
180 | return !$this->userIdentityUtils->isNamed( $rc->getPerformerIdentity() ); |
181 | } |
182 | ], |
183 | [ |
184 | 'name' => 'registered', |
185 | 'label' => 'rcfilters-filter-user-experience-level-registered-label', |
186 | 'description' => 'rcfilters-filter-user-experience-level-registered-description', |
187 | 'cssClassSuffix' => 'user-registered', |
188 | 'isRowApplicableCallable' => function ( IContextSource $ctx, RecentChange $rc ) { |
189 | return $this->userIdentityUtils->isNamed( $rc->getPerformerIdentity() ); |
190 | } |
191 | ], |
192 | [ |
193 | 'name' => 'newcomer', |
194 | 'label' => 'rcfilters-filter-user-experience-level-newcomer-label', |
195 | 'description' => 'rcfilters-filter-user-experience-level-newcomer-description', |
196 | 'cssClassSuffix' => 'user-newcomer', |
197 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
198 | $performer = $rc->getPerformerIdentity(); |
199 | return $performer->isRegistered() && |
200 | MediaWikiServices::getInstance() |
201 | ->getUserFactory() |
202 | ->newFromUserIdentity( $performer ) |
203 | ->getExperienceLevel() === 'newcomer'; |
204 | } |
205 | ], |
206 | [ |
207 | 'name' => 'learner', |
208 | 'label' => 'rcfilters-filter-user-experience-level-learner-label', |
209 | 'description' => 'rcfilters-filter-user-experience-level-learner-description', |
210 | 'cssClassSuffix' => 'user-learner', |
211 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
212 | $performer = $rc->getPerformerIdentity(); |
213 | return $performer->isRegistered() && |
214 | MediaWikiServices::getInstance() |
215 | ->getUserFactory() |
216 | ->newFromUserIdentity( $performer ) |
217 | ->getExperienceLevel() === 'learner'; |
218 | }, |
219 | ], |
220 | [ |
221 | 'name' => 'experienced', |
222 | 'label' => 'rcfilters-filter-user-experience-level-experienced-label', |
223 | 'description' => 'rcfilters-filter-user-experience-level-experienced-description', |
224 | 'cssClassSuffix' => 'user-experienced', |
225 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
226 | $performer = $rc->getPerformerIdentity(); |
227 | return $performer->isRegistered() && |
228 | MediaWikiServices::getInstance() |
229 | ->getUserFactory() |
230 | ->newFromUserIdentity( $performer ) |
231 | ->getExperienceLevel() === 'experienced'; |
232 | }, |
233 | ] |
234 | ], |
235 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
236 | 'queryCallable' => [ $this, 'filterOnUserExperienceLevel' ], |
237 | ], |
238 | |
239 | [ |
240 | 'name' => 'authorship', |
241 | 'title' => 'rcfilters-filtergroup-authorship', |
242 | 'class' => ChangesListBooleanFilterGroup::class, |
243 | 'filters' => [ |
244 | [ |
245 | 'name' => 'hidemyself', |
246 | 'label' => 'rcfilters-filter-editsbyself-label', |
247 | 'description' => 'rcfilters-filter-editsbyself-description', |
248 | // rcshowhidemine-show, rcshowhidemine-hide, |
249 | // wlshowhidemine |
250 | 'showHideSuffix' => 'showhidemine', |
251 | 'default' => false, |
252 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
253 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
254 | ) { |
255 | $user = $ctx->getUser(); |
256 | $conds[] = $dbr->expr( 'actor_name', '!=', $user->getName() ); |
257 | $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; |
258 | }, |
259 | 'cssClassSuffix' => 'self', |
260 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
261 | return $ctx->getUser()->equals( $rc->getPerformerIdentity() ); |
262 | }, |
263 | ], |
264 | [ |
265 | 'name' => 'hidebyothers', |
266 | 'label' => 'rcfilters-filter-editsbyother-label', |
267 | 'description' => 'rcfilters-filter-editsbyother-description', |
268 | 'default' => false, |
269 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
270 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
271 | ) { |
272 | $user = $ctx->getUser(); |
273 | if ( $user->isAnon() ) { |
274 | $conds['actor_name'] = $user->getName(); |
275 | } else { |
276 | $conds['actor_user'] = $user->getId(); |
277 | } |
278 | $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; |
279 | }, |
280 | 'cssClassSuffix' => 'others', |
281 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
282 | return !$ctx->getUser()->equals( $rc->getPerformerIdentity() ); |
283 | }, |
284 | ] |
285 | ] |
286 | ], |
287 | |
288 | [ |
289 | 'name' => 'automated', |
290 | 'title' => 'rcfilters-filtergroup-automated', |
291 | 'class' => ChangesListBooleanFilterGroup::class, |
292 | 'filters' => [ |
293 | [ |
294 | 'name' => 'hidebots', |
295 | 'label' => 'rcfilters-filter-bots-label', |
296 | 'description' => 'rcfilters-filter-bots-description', |
297 | // rcshowhidebots-show, rcshowhidebots-hide, |
298 | // wlshowhidebots |
299 | 'showHideSuffix' => 'showhidebots', |
300 | 'default' => false, |
301 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
302 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
303 | ) { |
304 | $conds['rc_bot'] = 0; |
305 | }, |
306 | 'cssClassSuffix' => 'bot', |
307 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
308 | return $rc->getAttribute( 'rc_bot' ); |
309 | }, |
310 | ], |
311 | [ |
312 | 'name' => 'hidehumans', |
313 | 'label' => 'rcfilters-filter-humans-label', |
314 | 'description' => 'rcfilters-filter-humans-description', |
315 | 'default' => false, |
316 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
317 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
318 | ) { |
319 | $conds['rc_bot'] = 1; |
320 | }, |
321 | 'cssClassSuffix' => 'human', |
322 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
323 | return !$rc->getAttribute( 'rc_bot' ); |
324 | }, |
325 | ] |
326 | ] |
327 | ], |
328 | |
329 | // significance (conditional) |
330 | |
331 | [ |
332 | 'name' => 'significance', |
333 | 'title' => 'rcfilters-filtergroup-significance', |
334 | 'class' => ChangesListBooleanFilterGroup::class, |
335 | 'priority' => -6, |
336 | 'filters' => [ |
337 | [ |
338 | 'name' => 'hideminor', |
339 | 'label' => 'rcfilters-filter-minor-label', |
340 | 'description' => 'rcfilters-filter-minor-description', |
341 | // rcshowhideminor-show, rcshowhideminor-hide, |
342 | // wlshowhideminor |
343 | 'showHideSuffix' => 'showhideminor', |
344 | 'default' => false, |
345 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
346 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
347 | ) { |
348 | $conds[] = $dbr->expr( 'rc_minor', '=', 0 ); |
349 | }, |
350 | 'cssClassSuffix' => 'minor', |
351 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
352 | return $rc->getAttribute( 'rc_minor' ); |
353 | } |
354 | ], |
355 | [ |
356 | 'name' => 'hidemajor', |
357 | 'label' => 'rcfilters-filter-major-label', |
358 | 'description' => 'rcfilters-filter-major-description', |
359 | 'default' => false, |
360 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
361 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
362 | ) { |
363 | $conds[] = $dbr->expr( 'rc_minor', '=', 1 ); |
364 | }, |
365 | 'cssClassSuffix' => 'major', |
366 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
367 | return !$rc->getAttribute( 'rc_minor' ); |
368 | } |
369 | ] |
370 | ] |
371 | ], |
372 | |
373 | [ |
374 | 'name' => 'lastRevision', |
375 | 'title' => 'rcfilters-filtergroup-lastrevision', |
376 | 'class' => ChangesListBooleanFilterGroup::class, |
377 | 'priority' => -7, |
378 | 'filters' => [ |
379 | [ |
380 | 'name' => 'hidelastrevision', |
381 | 'label' => 'rcfilters-filter-lastrevision-label', |
382 | 'description' => 'rcfilters-filter-lastrevision-description', |
383 | 'default' => false, |
384 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
385 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
386 | ) use ( $nonRevisionTypes ) { |
387 | $conds[] = $dbr->expr( 'rc_this_oldid', '!=', new RawSQLValue( 'page_latest' ) ) |
388 | ->or( 'rc_type', '=', $nonRevisionTypes ); |
389 | }, |
390 | 'cssClassSuffix' => 'last', |
391 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
392 | return $rc->getAttribute( 'rc_this_oldid' ) === $rc->getAttribute( 'page_latest' ); |
393 | } |
394 | ], |
395 | [ |
396 | 'name' => 'hidepreviousrevisions', |
397 | 'label' => 'rcfilters-filter-previousrevision-label', |
398 | 'description' => 'rcfilters-filter-previousrevision-description', |
399 | 'default' => false, |
400 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
401 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
402 | ) use ( $nonRevisionTypes ) { |
403 | $conds[] = $dbr->expr( 'rc_this_oldid', '=', new RawSQLValue( 'page_latest' ) ) |
404 | ->or( 'rc_type', '=', $nonRevisionTypes ); |
405 | }, |
406 | 'cssClassSuffix' => 'previous', |
407 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
408 | return $rc->getAttribute( 'rc_this_oldid' ) !== $rc->getAttribute( 'page_latest' ); |
409 | } |
410 | ] |
411 | ] |
412 | ], |
413 | |
414 | // With extensions, there can be change types that will not be hidden by any of these. |
415 | [ |
416 | 'name' => 'changeType', |
417 | 'title' => 'rcfilters-filtergroup-changetype', |
418 | 'class' => ChangesListBooleanFilterGroup::class, |
419 | 'priority' => -8, |
420 | 'filters' => [ |
421 | [ |
422 | 'name' => 'hidepageedits', |
423 | 'label' => 'rcfilters-filter-pageedits-label', |
424 | 'description' => 'rcfilters-filter-pageedits-description', |
425 | 'default' => false, |
426 | 'priority' => -2, |
427 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
428 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
429 | ) { |
430 | $conds[] = $dbr->expr( 'rc_type', '!=', RC_EDIT ); |
431 | }, |
432 | 'cssClassSuffix' => 'src-mw-edit', |
433 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
434 | return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_EDIT; |
435 | }, |
436 | ], |
437 | [ |
438 | 'name' => 'hidenewpages', |
439 | 'label' => 'rcfilters-filter-newpages-label', |
440 | 'description' => 'rcfilters-filter-newpages-description', |
441 | 'default' => false, |
442 | 'priority' => -3, |
443 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
444 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
445 | ) { |
446 | $conds[] = $dbr->expr( 'rc_type', '!=', RC_NEW ); |
447 | }, |
448 | 'cssClassSuffix' => 'src-mw-new', |
449 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
450 | return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW; |
451 | }, |
452 | ], |
453 | |
454 | // hidecategorization |
455 | |
456 | [ |
457 | 'name' => 'hidelog', |
458 | 'label' => 'rcfilters-filter-logactions-label', |
459 | 'description' => 'rcfilters-filter-logactions-description', |
460 | 'default' => false, |
461 | 'priority' => -5, |
462 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
463 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
464 | ) { |
465 | $conds[] = $dbr->expr( 'rc_type', '!=', RC_LOG ); |
466 | }, |
467 | 'cssClassSuffix' => 'src-mw-log', |
468 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
469 | return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_LOG; |
470 | } |
471 | ], |
472 | [ |
473 | 'name' => 'hidenewuserlog', |
474 | 'label' => 'rcfilters-filter-accountcreations-label', |
475 | 'description' => 'rcfilters-filter-accountcreations-description', |
476 | 'default' => false, |
477 | 'priority' => -6, |
478 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
479 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
480 | ) { |
481 | $conds[] = $dbr->expr( 'rc_log_type', '!=', 'newusers' ) |
482 | ->or( 'rc_log_type', '=', null ); |
483 | }, |
484 | 'cssClassSuffix' => 'src-mw-newuserlog', |
485 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
486 | return $rc->getAttribute( 'rc_log_type' ) === "newusers"; |
487 | }, |
488 | ], |
489 | ], |
490 | ], |
491 | |
492 | ]; |
493 | |
494 | $this->legacyReviewStatusFilterGroupDefinition = [ |
495 | [ |
496 | 'name' => 'legacyReviewStatus', |
497 | 'title' => 'rcfilters-filtergroup-reviewstatus', |
498 | 'class' => ChangesListBooleanFilterGroup::class, |
499 | 'filters' => [ |
500 | [ |
501 | 'name' => 'hidepatrolled', |
502 | // rcshowhidepatr-show, rcshowhidepatr-hide |
503 | // wlshowhidepatr |
504 | 'showHideSuffix' => 'showhidepatr', |
505 | 'default' => false, |
506 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
507 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
508 | ) { |
509 | $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED; |
510 | }, |
511 | 'isReplacedInStructuredUi' => true, |
512 | ], |
513 | [ |
514 | 'name' => 'hideunpatrolled', |
515 | 'default' => false, |
516 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
517 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
518 | ) { |
519 | $conds[] = $dbr->expr( 'rc_patrolled', '!=', RecentChange::PRC_UNPATROLLED ); |
520 | }, |
521 | 'isReplacedInStructuredUi' => true, |
522 | ], |
523 | ], |
524 | ] |
525 | ]; |
526 | |
527 | $this->reviewStatusFilterGroupDefinition = [ |
528 | [ |
529 | 'name' => 'reviewStatus', |
530 | 'title' => 'rcfilters-filtergroup-reviewstatus', |
531 | 'class' => ChangesListStringOptionsFilterGroup::class, |
532 | 'isFullCoverage' => true, |
533 | 'priority' => -5, |
534 | 'filters' => [ |
535 | [ |
536 | 'name' => 'unpatrolled', |
537 | 'label' => 'rcfilters-filter-reviewstatus-unpatrolled-label', |
538 | 'description' => 'rcfilters-filter-reviewstatus-unpatrolled-description', |
539 | 'cssClassSuffix' => 'reviewstatus-unpatrolled', |
540 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
541 | return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_UNPATROLLED; |
542 | }, |
543 | ], |
544 | [ |
545 | 'name' => 'manual', |
546 | 'label' => 'rcfilters-filter-reviewstatus-manual-label', |
547 | 'description' => 'rcfilters-filter-reviewstatus-manual-description', |
548 | 'cssClassSuffix' => 'reviewstatus-manual', |
549 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
550 | return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_PATROLLED; |
551 | }, |
552 | ], |
553 | [ |
554 | 'name' => 'auto', |
555 | 'label' => 'rcfilters-filter-reviewstatus-auto-label', |
556 | 'description' => 'rcfilters-filter-reviewstatus-auto-description', |
557 | 'cssClassSuffix' => 'reviewstatus-auto', |
558 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
559 | return $rc->getAttribute( 'rc_patrolled' ) == RecentChange::PRC_AUTOPATROLLED; |
560 | }, |
561 | ], |
562 | ], |
563 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
564 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
565 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selected |
566 | ) { |
567 | if ( $selected === [] ) { |
568 | return; |
569 | } |
570 | $rcPatrolledValues = [ |
571 | 'unpatrolled' => RecentChange::PRC_UNPATROLLED, |
572 | 'manual' => RecentChange::PRC_PATROLLED, |
573 | 'auto' => RecentChange::PRC_AUTOPATROLLED, |
574 | ]; |
575 | // e.g. rc_patrolled IN (0, 2) |
576 | $conds['rc_patrolled'] = array_map( static function ( $s ) use ( $rcPatrolledValues ) { |
577 | return $rcPatrolledValues[ $s ]; |
578 | }, $selected ); |
579 | } |
580 | ] |
581 | ]; |
582 | |
583 | $this->hideCategorizationFilterDefinition = [ |
584 | 'name' => 'hidecategorization', |
585 | 'label' => 'rcfilters-filter-categorization-label', |
586 | 'description' => 'rcfilters-filter-categorization-description', |
587 | // rcshowhidecategorization-show, rcshowhidecategorization-hide. |
588 | // wlshowhidecategorization |
589 | 'showHideSuffix' => 'showhidecategorization', |
590 | 'default' => false, |
591 | 'priority' => -4, |
592 | 'queryCallable' => static function ( string $specialClassName, IContextSource $ctx, |
593 | IReadableDatabase $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
594 | ) { |
595 | $conds[] = $dbr->expr( 'rc_type', '!=', RC_CATEGORIZE ); |
596 | }, |
597 | 'cssClassSuffix' => 'src-mw-categorize', |
598 | 'isRowApplicableCallable' => static function ( IContextSource $ctx, RecentChange $rc ) { |
599 | return $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_CATEGORIZE; |
600 | }, |
601 | ]; |
602 | } |
603 | |
604 | /** |
605 | * Removes registration filters from filterGroupDefinitions |
606 | * @return void |
607 | */ |
608 | private function removeRegistrationFilterDefinitions(): void { |
609 | foreach ( $this->filterGroupDefinitions as $key => $value ) { |
610 | if ( $value['name'] == "userExpLevel" ) { |
611 | $this->filterGroupDefinitions[ $key ][ 'filters' ] = array_filter( |
612 | $this->filterGroupDefinitions[ $key ][ 'filters' ], |
613 | fn ( $val, $key ) => $val[ 'name' ] != 'registered' |
614 | && $val[ 'name' ] != 'unregistered', ARRAY_FILTER_USE_BOTH ); |
615 | } |
616 | } |
617 | } |
618 | |
619 | /** |
620 | * Check if filters are in conflict and guaranteed to return no results. |
621 | * |
622 | * @return bool |
623 | */ |
624 | protected function areFiltersInConflict() { |
625 | $opts = $this->getOptions(); |
626 | foreach ( $this->getFilterGroups() as $group ) { |
627 | if ( $group->getConflictingGroups() ) { |
628 | wfLogWarning( |
629 | $group->getName() . |
630 | " specifies conflicts with other groups but these are not supported yet." |
631 | ); |
632 | } |
633 | |
634 | foreach ( $group->getConflictingFilters() as $conflictingFilter ) { |
635 | if ( $conflictingFilter->activelyInConflictWithGroup( $group, $opts ) ) { |
636 | return true; |
637 | } |
638 | } |
639 | |
640 | foreach ( $group->getFilters() as $filter ) { |
641 | foreach ( $filter->getConflictingFilters() as $conflictingFilter ) { |
642 | if ( |
643 | $conflictingFilter->activelyInConflictWithFilter( $filter, $opts ) && |
644 | $filter->activelyInConflictWithFilter( $conflictingFilter, $opts ) |
645 | ) { |
646 | return true; |
647 | } |
648 | } |
649 | |
650 | } |
651 | |
652 | } |
653 | |
654 | return false; |
655 | } |
656 | |
657 | /** |
658 | * @param string|null $subpage |
659 | */ |
660 | public function execute( $subpage ) { |
661 | $this->rcSubpage = $subpage; |
662 | |
663 | if ( $this->considerActionsForDefaultSavedQuery( $subpage ) ) { |
664 | // Don't bother rendering the page if we'll be performing a redirect (T330100). |
665 | return; |
666 | } |
667 | |
668 | // Enable OOUI and module for the clock icon. |
669 | if ( $this->getConfig()->get( MainConfigNames::WatchlistExpiry ) && !$this->including() ) { |
670 | $this->getOutput()->enableOOUI(); |
671 | $this->getOutput()->addModules( 'mediawiki.special.changeslist.watchlistexpiry' ); |
672 | } |
673 | |
674 | $opts = $this->getOptions(); |
675 | try { |
676 | $rows = $this->getRows(); |
677 | if ( $rows === false ) { |
678 | $rows = new FakeResultWrapper( [] ); |
679 | } |
680 | |
681 | // Used by Structured UI app to get results without MW chrome |
682 | if ( $this->getRequest()->getRawVal( 'action' ) === 'render' ) { |
683 | $this->getOutput()->setArticleBodyOnly( true ); |
684 | } |
685 | |
686 | // Used by "live update" and "view newest" to check |
687 | // if there's new changes with minimal data transfer |
688 | if ( $this->getRequest()->getBool( 'peek' ) ) { |
689 | $code = $rows->numRows() > 0 ? 200 : 204; |
690 | $this->getOutput()->setStatusCode( $code ); |
691 | |
692 | if ( $this->getUser()->isAnon() !== |
693 | $this->getRequest()->getFuzzyBool( 'isAnon' ) |
694 | ) { |
695 | $this->getOutput()->setStatusCode( 205 ); |
696 | } |
697 | |
698 | return; |
699 | } |
700 | |
701 | $services = MediaWikiServices::getInstance(); |
702 | $logFormatterFactory = $services->getLogFormatterFactory(); |
703 | $linkBatchFactory = $services->getLinkBatchFactory(); |
704 | $batch = $linkBatchFactory->newLinkBatch(); |
705 | $userNames = []; |
706 | foreach ( $rows as $row ) { |
707 | $batch->add( NS_USER, $row->rc_user_text ); |
708 | $batch->add( NS_USER_TALK, $row->rc_user_text ); |
709 | $userNames[] = $row->rc_user_text; |
710 | $batch->add( $row->rc_namespace, $row->rc_title ); |
711 | if ( $row->rc_source === RecentChange::SRC_LOG ) { |
712 | $formatter = $logFormatterFactory->newFromRow( $row ); |
713 | foreach ( $formatter->getPreloadTitles() as $title ) { |
714 | $batch->addObj( $title ); |
715 | if ( $title->inNamespace( NS_USER ) || $title->inNamespace( NS_USER_TALK ) ) { |
716 | $userNames[] = $title->getText(); |
717 | } |
718 | } |
719 | } |
720 | } |
721 | $batch->execute(); |
722 | foreach ( UserArray::newFromNames( $userNames ) as $_ ) { |
723 | // Trigger UserEditTracker::setCachedUserEditCount via User::loadFromRow |
724 | // Preloads edit count for User::getExperienceLevel() and Linker::userToolLinks() |
725 | } |
726 | |
727 | $this->setHeaders(); |
728 | $this->outputHeader(); |
729 | $this->addModules(); |
730 | $this->webOutput( $rows, $opts ); |
731 | |
732 | $rows->free(); |
733 | } catch ( DBQueryTimeoutError $timeoutException ) { |
734 | MWExceptionHandler::logException( $timeoutException ); |
735 | |
736 | $this->setHeaders(); |
737 | $this->outputHeader(); |
738 | $this->addModules(); |
739 | |
740 | $this->getOutput()->setStatusCode( 500 ); |
741 | $this->webOutputHeader( 0, $opts ); |
742 | $this->outputTimeout(); |
743 | } |
744 | |
745 | $this->includeRcFiltersApp(); |
746 | } |
747 | |
748 | /** |
749 | * Set the temp user config. |
750 | * |
751 | * @internal |
752 | * @param TempUserConfig $tempUserConfig |
753 | * @since 1.42 |
754 | */ |
755 | public function setTempUserConfig( TempUserConfig $tempUserConfig ) { |
756 | $this->tempUserConfig = $tempUserConfig; |
757 | } |
758 | |
759 | /** |
760 | * Check whether or not the page should load defaults, and if so, whether |
761 | * a default saved query is relevant to be redirected to. If it is relevant, |
762 | * redirect properly with all necessary query parameters. |
763 | * |
764 | * @param string $subpage |
765 | * @return bool Whether a redirect will be performed. |
766 | */ |
767 | protected function considerActionsForDefaultSavedQuery( $subpage ) { |
768 | if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) { |
769 | return false; |
770 | } |
771 | |
772 | $knownParams = $this->getRequest()->getValues( |
773 | ...array_keys( $this->getOptions()->getAllValues() ) |
774 | ); |
775 | |
776 | // HACK: Temporarily until we can properly define "sticky" filters and parameters, |
777 | // we need to exclude several parameters we know should not be counted towards preventing |
778 | // the loading of defaults. |
779 | $excludedParams = [ 'limit' => '', 'days' => '', 'enhanced' => '', 'from' => '' ]; |
780 | $knownParams = array_diff_key( $knownParams, $excludedParams ); |
781 | |
782 | if ( |
783 | // If there are NO known parameters in the URL request |
784 | // (that are not excluded) then we need to check into loading |
785 | // the default saved query |
786 | count( $knownParams ) === 0 |
787 | ) { |
788 | $prefJson = MediaWikiServices::getInstance() |
789 | ->getUserOptionsLookup() |
790 | ->getOption( $this->getUser(), $this->getSavedQueriesPreferenceName() ); |
791 | |
792 | // Get the saved queries data and parse it |
793 | $savedQueries = $prefJson ? FormatJson::decode( $prefJson, true ) : false; |
794 | |
795 | if ( $savedQueries && isset( $savedQueries[ 'default' ] ) ) { |
796 | // Only load queries that are 'version' 2, since those |
797 | // have parameter representation |
798 | if ( isset( $savedQueries[ 'version' ] ) && $savedQueries[ 'version' ] === '2' ) { |
799 | $savedQueryDefaultID = $savedQueries[ 'default' ]; |
800 | $defaultQuery = $savedQueries[ 'queries' ][ $savedQueryDefaultID ][ 'data' ]; |
801 | |
802 | // Build the entire parameter list |
803 | $query = array_merge( |
804 | $defaultQuery[ 'params' ], |
805 | $defaultQuery[ 'highlights' ], |
806 | [ |
807 | 'urlversion' => '2', |
808 | ] |
809 | ); |
810 | // Add to the query any parameters that we may have ignored before |
811 | // but are still valid and requested in the URL |
812 | $query = array_merge( $this->getRequest()->getQueryValues(), $query ); |
813 | unset( $query[ 'title' ] ); |
814 | $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) ); |
815 | |
816 | // Signal that we only need to redirect to the full URL |
817 | // and can skip rendering the actual page (T330100). |
818 | return true; |
819 | } else { |
820 | // There's a default, but the version is not 2, and the server can't |
821 | // actually recognize the query itself. This happens if it is before |
822 | // the conversion, so we need to tell the UI to reload saved query as |
823 | // it does the conversion to version 2 |
824 | $this->getOutput()->addJsConfigVars( |
825 | 'wgStructuredChangeFiltersDefaultSavedQueryExists', |
826 | true |
827 | ); |
828 | |
829 | // Add the class that tells the frontend it is still loading |
830 | // another query |
831 | $this->getOutput()->addBodyClasses( 'mw-rcfilters-ui-loading' ); |
832 | } |
833 | } |
834 | } |
835 | |
836 | return false; |
837 | } |
838 | |
839 | /** |
840 | * @see \MediaWiki\MainConfigSchema::RCLinkDays and \MediaWiki\MainConfigSchema::RCFilterByAge. |
841 | * @return int[] |
842 | */ |
843 | protected function getLinkDays() { |
844 | $linkDays = $this->getConfig()->get( MainConfigNames::RCLinkDays ); |
845 | $filterByAge = $this->getConfig()->get( MainConfigNames::RCFilterByAge ); |
846 | $maxAge = $this->getConfig()->get( MainConfigNames::RCMaxAge ); |
847 | if ( $filterByAge ) { |
848 | // Trim it to only links which are within $wgRCMaxAge. |
849 | // Note that we allow one link higher than the max for things like |
850 | // "age 56 days" being accessible through the "60 days" link. |
851 | sort( $linkDays ); |
852 | |
853 | $maxAgeDays = $maxAge / ( 3600 * 24 ); |
854 | foreach ( $linkDays as $i => $days ) { |
855 | if ( $days >= $maxAgeDays ) { |
856 | array_splice( $linkDays, $i + 1 ); |
857 | break; |
858 | } |
859 | } |
860 | } |
861 | |
862 | return $linkDays; |
863 | } |
864 | |
865 | /** |
866 | * Include the modules and configuration for the RCFilters app. |
867 | * Conditional on the user having the feature enabled. |
868 | * |
869 | * If it is disabled, add a <body> class marking that |
870 | */ |
871 | protected function includeRcFiltersApp() { |
872 | $out = $this->getOutput(); |
873 | if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) { |
874 | $jsData = $this->getStructuredFilterJsData(); |
875 | $messages = []; |
876 | foreach ( $jsData['messageKeys'] as $key ) { |
877 | $messages[$key] = $this->msg( $key )->plain(); |
878 | } |
879 | |
880 | $out->addBodyClasses( 'mw-rcfilters-enabled' ); |
881 | $collapsed = MediaWikiServices::getInstance()->getUserOptionsLookup() |
882 | ->getBoolOption( $this->getUser(), $this->getCollapsedPreferenceName() ); |
883 | if ( $collapsed ) { |
884 | $out->addBodyClasses( 'mw-rcfilters-collapsed' ); |
885 | } |
886 | |
887 | // These config and message exports should be moved into a ResourceLoader data module (T201574) |
888 | $out->addJsConfigVars( 'wgStructuredChangeFilters', $jsData['groups'] ); |
889 | $out->addJsConfigVars( 'wgStructuredChangeFiltersMessages', $messages ); |
890 | $out->addJsConfigVars( 'wgStructuredChangeFiltersCollapsedState', $collapsed ); |
891 | |
892 | $out->addJsConfigVars( |
893 | 'StructuredChangeFiltersDisplayConfig', |
894 | [ |
895 | 'maxDays' => // Translate to days |
896 | (int)$this->getConfig()->get( MainConfigNames::RCMaxAge ) / ( 24 * 3600 ), |
897 | 'limitArray' => $this->getConfig()->get( MainConfigNames::RCLinkLimits ), |
898 | 'limitDefault' => $this->getDefaultLimit(), |
899 | 'daysArray' => $this->getLinkDays(), |
900 | 'daysDefault' => $this->getDefaultDays(), |
901 | ] |
902 | ); |
903 | |
904 | $out->addJsConfigVars( |
905 | 'wgStructuredChangeFiltersSavedQueriesPreferenceName', |
906 | $this->getSavedQueriesPreferenceName() |
907 | ); |
908 | $out->addJsConfigVars( |
909 | 'wgStructuredChangeFiltersLimitPreferenceName', |
910 | $this->getLimitPreferenceName() |
911 | ); |
912 | $out->addJsConfigVars( |
913 | 'wgStructuredChangeFiltersDaysPreferenceName', |
914 | $this->getDefaultDaysPreferenceName() |
915 | ); |
916 | $out->addJsConfigVars( |
917 | 'wgStructuredChangeFiltersCollapsedPreferenceName', |
918 | $this->getCollapsedPreferenceName() |
919 | ); |
920 | } else { |
921 | $out->addBodyClasses( 'mw-rcfilters-disabled' ); |
922 | } |
923 | } |
924 | |
925 | /** |
926 | * Get essential data about getRcFiltersConfigVars() for change detection. |
927 | * |
928 | * @internal For use by Resources.php only. |
929 | * @see Module::getDefinitionSummary() and Module::getVersionHash() |
930 | * @param RL\Context $context |
931 | * @return array |
932 | */ |
933 | public static function getRcFiltersConfigSummary( RL\Context $context ) { |
934 | $lang = MediaWikiServices::getInstance()->getLanguageFactory() |
935 | ->getLanguage( $context->getLanguage() ); |
936 | return [ |
937 | // Reduce version computation by avoiding Message parsing |
938 | 'RCFiltersChangeTags' => ChangeTags::getChangeTagListSummary( $context, $lang ), |
939 | 'StructuredChangeFiltersEditWatchlistUrl' => |
940 | SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL() |
941 | ]; |
942 | } |
943 | |
944 | /** |
945 | * Get config vars to export with the mediawiki.rcfilters.filters.ui module. |
946 | * |
947 | * @internal For use by Resources.php only. |
948 | * @param RL\Context $context |
949 | * @return array |
950 | */ |
951 | public static function getRcFiltersConfigVars( RL\Context $context ) { |
952 | $lang = MediaWikiServices::getInstance()->getLanguageFactory() |
953 | ->getLanguage( $context->getLanguage() ); |
954 | return [ |
955 | 'RCFiltersChangeTags' => ChangeTags::getChangeTagList( $context, $lang ), |
956 | 'StructuredChangeFiltersEditWatchlistUrl' => |
957 | SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL() |
958 | ]; |
959 | } |
960 | |
961 | /** |
962 | * Add the "no results" message to the output |
963 | */ |
964 | protected function outputNoResults() { |
965 | $this->getOutput()->addHTML( |
966 | Html::rawElement( |
967 | 'div', |
968 | [ 'class' => 'mw-changeslist-empty' ], |
969 | $this->msg( 'recentchanges-noresult' )->parse() |
970 | ) |
971 | ); |
972 | } |
973 | |
974 | /** |
975 | * Add the "timeout" message to the output |
976 | */ |
977 | protected function outputTimeout() { |
978 | $this->getOutput()->addHTML( |
979 | '<div class="mw-changeslist-empty mw-changeslist-timeout">' . |
980 | $this->msg( 'recentchanges-timeout' )->parse() . |
981 | '</div>' |
982 | ); |
983 | } |
984 | |
985 | /** |
986 | * Get the database result for this special page instance. Used by ApiFeedRecentChanges. |
987 | * |
988 | * @return IResultWrapper|false |
989 | */ |
990 | public function getRows() { |
991 | $opts = $this->getOptions(); |
992 | |
993 | $tables = []; |
994 | $fields = []; |
995 | $conds = []; |
996 | $query_options = []; |
997 | $join_conds = []; |
998 | $this->buildQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); |
999 | |
1000 | return $this->doMainQuery( $tables, $fields, $conds, $query_options, $join_conds, $opts ); |
1001 | } |
1002 | |
1003 | /** |
1004 | * Get the current FormOptions for this request |
1005 | * |
1006 | * @return FormOptions |
1007 | */ |
1008 | public function getOptions() { |
1009 | if ( $this->rcOptions === null ) { |
1010 | $this->rcOptions = $this->setup( $this->rcSubpage ); |
1011 | } |
1012 | |
1013 | return $this->rcOptions; |
1014 | } |
1015 | |
1016 | /** |
1017 | * Register all filters and their groups (including those from hooks), plus handle |
1018 | * conflicts and defaults. |
1019 | * |
1020 | * You might want to customize these in the same method, in subclasses. You can |
1021 | * call getFilterGroup to access a group, and (on the group) getFilter to access a |
1022 | * filter, then make necessary modfications to the filter or group (e.g. with |
1023 | * setDefault). |
1024 | */ |
1025 | protected function registerFilters() { |
1026 | $isRegistrationRequiredToEdit = !MediaWikiServices::getInstance() |
1027 | ->getPermissionManager() |
1028 | ->isEveryoneAllowed( "edit" ); |
1029 | if ( $isRegistrationRequiredToEdit ) { |
1030 | $this->removeRegistrationFilterDefinitions(); |
1031 | } |
1032 | $this->registerFiltersFromDefinitions( $this->filterGroupDefinitions ); |
1033 | |
1034 | // Make sure this is not being transcluded (we don't want to show this |
1035 | // information to all users just because the user that saves the edit can |
1036 | // patrol or is logged in) |
1037 | if ( !$this->including() && $this->getUser()->useRCPatrol() ) { |
1038 | $this->registerFiltersFromDefinitions( $this->legacyReviewStatusFilterGroupDefinition ); |
1039 | $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition ); |
1040 | } |
1041 | |
1042 | $changeTypeGroup = $this->getFilterGroup( 'changeType' ); |
1043 | |
1044 | $categoryFilter = null; |
1045 | if ( $this->getConfig()->get( MainConfigNames::RCWatchCategoryMembership ) ) { |
1046 | $transformedHideCategorizationDef = $this->transformFilterDefinition( |
1047 | $this->hideCategorizationFilterDefinition |
1048 | ); |
1049 | |
1050 | $transformedHideCategorizationDef['group'] = $changeTypeGroup; |
1051 | |
1052 | $categoryFilter = new ChangesListBooleanFilter( |
1053 | $transformedHideCategorizationDef |
1054 | ); |
1055 | } |
1056 | |
1057 | $this->getHookRunner()->onChangesListSpecialPageStructuredFilters( $this ); |
1058 | |
1059 | $this->registerFiltersFromDefinitions( [] ); |
1060 | |
1061 | $userExperienceLevel = $this->getFilterGroup( 'userExpLevel' ); |
1062 | if ( !$isRegistrationRequiredToEdit ) { |
1063 | $registered = $userExperienceLevel->getFilter( 'registered' ); |
1064 | $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'newcomer' ) ); |
1065 | $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'learner' ) ); |
1066 | $registered->setAsSupersetOf( $userExperienceLevel->getFilter( 'experienced' ) ); |
1067 | } |
1068 | |
1069 | $logactionsFilter = $changeTypeGroup->getFilter( 'hidelog' ); |
1070 | $lognewuserFilter = $changeTypeGroup->getFilter( 'hidenewuserlog' ); |
1071 | $pagecreationFilter = $changeTypeGroup->getFilter( 'hidenewpages' ); |
1072 | |
1073 | $significanceTypeGroup = $this->getFilterGroup( 'significance' ); |
1074 | $hideMinorFilter = $significanceTypeGroup->getFilter( 'hideminor' ); |
1075 | |
1076 | if ( $categoryFilter !== null ) { |
1077 | $hideMinorFilter->conflictsWith( |
1078 | $categoryFilter, |
1079 | 'rcfilters-hideminor-conflicts-typeofchange-global', |
1080 | 'rcfilters-hideminor-conflicts-typeofchange', |
1081 | 'rcfilters-typeofchange-conflicts-hideminor' |
1082 | ); |
1083 | } |
1084 | $hideMinorFilter->conflictsWith( |
1085 | $logactionsFilter, |
1086 | 'rcfilters-hideminor-conflicts-typeofchange-global', |
1087 | 'rcfilters-hideminor-conflicts-typeofchange', |
1088 | 'rcfilters-typeofchange-conflicts-hideminor' |
1089 | ); |
1090 | $hideMinorFilter->conflictsWith( |
1091 | $lognewuserFilter, |
1092 | 'rcfilters-hideminor-conflicts-typeofchange-global', |
1093 | 'rcfilters-hideminor-conflicts-typeofchange', |
1094 | 'rcfilters-typeofchange-conflicts-hideminor' |
1095 | ); |
1096 | $hideMinorFilter->conflictsWith( |
1097 | $pagecreationFilter, |
1098 | 'rcfilters-hideminor-conflicts-typeofchange-global', |
1099 | 'rcfilters-hideminor-conflicts-typeofchange', |
1100 | 'rcfilters-typeofchange-conflicts-hideminor' |
1101 | ); |
1102 | } |
1103 | |
1104 | /** |
1105 | * Transforms filter definition to prepare it for constructor. |
1106 | * |
1107 | * See overrides of this method as well. |
1108 | * |
1109 | * @param array $filterDefinition Original filter definition |
1110 | * |
1111 | * @return array Transformed definition |
1112 | */ |
1113 | protected function transformFilterDefinition( array $filterDefinition ) { |
1114 | return $filterDefinition; |
1115 | } |
1116 | |
1117 | /** |
1118 | * Register filters from a definition object |
1119 | * |
1120 | * Array specifying groups and their filters; see Filter and |
1121 | * ChangesListFilterGroup constructors. |
1122 | * |
1123 | * There is light processing to simplify core maintenance. |
1124 | * @param array $definition |
1125 | * @phan-param array<int,array{class:class-string<ChangesListFilterGroup>,filters:array}> $definition |
1126 | */ |
1127 | protected function registerFiltersFromDefinitions( array $definition ) { |
1128 | $autoFillPriority = -1; |
1129 | foreach ( $definition as $groupDefinition ) { |
1130 | if ( !isset( $groupDefinition['priority'] ) ) { |
1131 | $groupDefinition['priority'] = $autoFillPriority; |
1132 | } else { |
1133 | // If it's explicitly specified, start over the auto-fill |
1134 | $autoFillPriority = $groupDefinition['priority']; |
1135 | } |
1136 | |
1137 | $autoFillPriority--; |
1138 | |
1139 | $className = $groupDefinition['class']; |
1140 | unset( $groupDefinition['class'] ); |
1141 | |
1142 | foreach ( $groupDefinition['filters'] as &$filterDefinition ) { |
1143 | $filterDefinition = $this->transformFilterDefinition( $filterDefinition ); |
1144 | } |
1145 | |
1146 | $this->registerFilterGroup( new $className( $groupDefinition ) ); |
1147 | } |
1148 | } |
1149 | |
1150 | /** |
1151 | * @return ChangesListBooleanFilter[] The legacy show/hide toggle filters |
1152 | */ |
1153 | protected function getLegacyShowHideFilters() { |
1154 | $filters = []; |
1155 | foreach ( $this->filterGroups as $group ) { |
1156 | if ( $group instanceof ChangesListBooleanFilterGroup ) { |
1157 | foreach ( $group->getFilters() as $key => $filter ) { |
1158 | if ( $filter->displaysOnUnstructuredUi() ) { |
1159 | $filters[ $key ] = $filter; |
1160 | } |
1161 | } |
1162 | } |
1163 | } |
1164 | return $filters; |
1165 | } |
1166 | |
1167 | /** |
1168 | * Register all the filters, including legacy hook-driven ones. |
1169 | * Then create a FormOptions object with options as specified by the user |
1170 | * |
1171 | * @param string $parameters |
1172 | * |
1173 | * @return FormOptions |
1174 | */ |
1175 | public function setup( $parameters ) { |
1176 | $this->registerFilters(); |
1177 | |
1178 | $opts = $this->getDefaultOptions(); |
1179 | |
1180 | $opts = $this->fetchOptionsFromRequest( $opts ); |
1181 | |
1182 | // Give precedence to subpage syntax |
1183 | if ( $parameters !== null ) { |
1184 | $this->parseParameters( $parameters, $opts ); |
1185 | } |
1186 | |
1187 | $this->validateOptions( $opts ); |
1188 | |
1189 | return $opts; |
1190 | } |
1191 | |
1192 | /** |
1193 | * Get a FormOptions object containing the default options. By default, returns |
1194 | * some basic options. The filters listed explicitly here are overridden in this |
1195 | * method, in subclasses, but most filters (e.g. hideminor, userExpLevel filters, |
1196 | * and more) are structured. Structured filters are overridden in registerFilters. |
1197 | * not here. |
1198 | * |
1199 | * @return FormOptions |
1200 | */ |
1201 | public function getDefaultOptions() { |
1202 | $opts = new FormOptions(); |
1203 | $structuredUI = $this->isStructuredFilterUiEnabled(); |
1204 | // If urlversion=2 is set, ignore the filter defaults and set them all to false/empty |
1205 | $useDefaults = $this->getRequest()->getInt( 'urlversion' ) !== 2; |
1206 | |
1207 | /** @var ChangesListFilterGroup $filterGroup */ |
1208 | foreach ( $this->filterGroups as $filterGroup ) { |
1209 | $filterGroup->addOptions( $opts, $useDefaults, $structuredUI ); |
1210 | } |
1211 | |
1212 | $opts->add( 'namespace', '', FormOptions::STRING ); |
1213 | // TODO: Rename this option to 'invertnamespaces'? |
1214 | $opts->add( 'invert', false ); |
1215 | $opts->add( 'associated', false ); |
1216 | $opts->add( 'urlversion', 1 ); |
1217 | $opts->add( 'tagfilter', '' ); |
1218 | $opts->add( 'inverttags', false ); |
1219 | |
1220 | $opts->add( 'days', $this->getDefaultDays(), FormOptions::FLOAT ); |
1221 | $opts->add( 'limit', $this->getDefaultLimit(), FormOptions::INT ); |
1222 | |
1223 | $opts->add( 'from', '' ); |
1224 | |
1225 | return $opts; |
1226 | } |
1227 | |
1228 | /** |
1229 | * Register a structured changes list filter group |
1230 | * |
1231 | * @param ChangesListFilterGroup $group |
1232 | */ |
1233 | public function registerFilterGroup( ChangesListFilterGroup $group ) { |
1234 | $groupName = $group->getName(); |
1235 | |
1236 | $this->filterGroups[$groupName] = $group; |
1237 | } |
1238 | |
1239 | /** |
1240 | * Gets the currently registered filters groups |
1241 | * |
1242 | * @return ChangesListFilterGroup[] Associative array of ChangesListFilterGroup objects, with group name as key |
1243 | */ |
1244 | protected function getFilterGroups() { |
1245 | return $this->filterGroups; |
1246 | } |
1247 | |
1248 | /** |
1249 | * Gets a specified ChangesListFilterGroup by name |
1250 | * |
1251 | * @param string $groupName Name of group |
1252 | * |
1253 | * @return ChangesListFilterGroup|null Group, or null if not registered |
1254 | */ |
1255 | public function getFilterGroup( $groupName ) { |
1256 | return $this->filterGroups[$groupName] ?? null; |
1257 | } |
1258 | |
1259 | // Currently, this intentionally only includes filters that display |
1260 | // in the structured UI. This can be changed easily, though, if we want |
1261 | // to include data on filters that use the unstructured UI. messageKeys is a |
1262 | // special top-level value, with the value being an array of the message keys to |
1263 | // send to the client. |
1264 | |
1265 | /** |
1266 | * Gets structured filter information needed by JS |
1267 | * |
1268 | * @return array Associative array |
1269 | * * array $return['groups'] Group data |
1270 | * * array $return['messageKeys'] Array of message keys |
1271 | */ |
1272 | public function getStructuredFilterJsData() { |
1273 | $output = [ |
1274 | 'groups' => [], |
1275 | 'messageKeys' => [], |
1276 | ]; |
1277 | |
1278 | usort( $this->filterGroups, static function ( ChangesListFilterGroup $a, ChangesListFilterGroup $b ) { |
1279 | return $b->getPriority() <=> $a->getPriority(); |
1280 | } ); |
1281 | |
1282 | foreach ( $this->filterGroups as $group ) { |
1283 | $groupOutput = $group->getJsData(); |
1284 | if ( $groupOutput !== null ) { |
1285 | $output['messageKeys'] = array_merge( |
1286 | $output['messageKeys'], |
1287 | $groupOutput['messageKeys'] |
1288 | ); |
1289 | |
1290 | unset( $groupOutput['messageKeys'] ); |
1291 | $output['groups'][] = $groupOutput; |
1292 | } |
1293 | } |
1294 | |
1295 | return $output; |
1296 | } |
1297 | |
1298 | /** |
1299 | * Fetch values for a FormOptions object from the WebRequest associated with this instance. |
1300 | * |
1301 | * Intended for subclassing, e.g. to add a backwards-compatibility layer. |
1302 | * |
1303 | * @param FormOptions $opts |
1304 | * @return FormOptions |
1305 | */ |
1306 | protected function fetchOptionsFromRequest( $opts ) { |
1307 | $opts->fetchValuesFromRequest( $this->getRequest() ); |
1308 | |
1309 | return $opts; |
1310 | } |
1311 | |
1312 | /** |
1313 | * Process $par and put options found in $opts. Used when including the page. |
1314 | * |
1315 | * @param string $par |
1316 | * @param FormOptions $opts |
1317 | */ |
1318 | public function parseParameters( $par, FormOptions $opts ) { |
1319 | $stringParameterNameSet = []; |
1320 | $hideParameterNameSet = []; |
1321 | |
1322 | // URL parameters can be per-group, like 'userExpLevel', |
1323 | // or per-filter, like 'hideminor'. |
1324 | |
1325 | foreach ( $this->filterGroups as $filterGroup ) { |
1326 | if ( $filterGroup instanceof ChangesListStringOptionsFilterGroup ) { |
1327 | $stringParameterNameSet[$filterGroup->getName()] = true; |
1328 | } elseif ( $filterGroup instanceof ChangesListBooleanFilterGroup ) { |
1329 | foreach ( $filterGroup->getFilters() as $filter ) { |
1330 | $hideParameterNameSet[$filter->getName()] = true; |
1331 | } |
1332 | } |
1333 | } |
1334 | |
1335 | $bits = preg_split( '/\s*,\s*/', trim( $par ) ); |
1336 | foreach ( $bits as $bit ) { |
1337 | $m = []; |
1338 | if ( isset( $hideParameterNameSet[$bit] ) ) { |
1339 | // hidefoo => hidefoo=true |
1340 | $opts[$bit] = true; |
1341 | } elseif ( isset( $hideParameterNameSet["hide$bit"] ) ) { |
1342 | // foo => hidefoo=false |
1343 | $opts["hide$bit"] = false; |
1344 | } elseif ( preg_match( '/^(.*)=(.*)$/', $bit, $m ) ) { |
1345 | if ( isset( $stringParameterNameSet[$m[1]] ) ) { |
1346 | $opts[$m[1]] = $m[2]; |
1347 | } |
1348 | } |
1349 | } |
1350 | } |
1351 | |
1352 | /** |
1353 | * Validate a FormOptions object generated by getDefaultOptions() with values already populated. |
1354 | * |
1355 | * @param FormOptions $opts |
1356 | */ |
1357 | public function validateOptions( FormOptions $opts ) { |
1358 | $isContradictory = $this->fixContradictoryOptions( $opts ); |
1359 | $isReplaced = $this->replaceOldOptions( $opts ); |
1360 | |
1361 | if ( $isContradictory || $isReplaced ) { |
1362 | $query = wfArrayToCgi( $this->convertParamsForLink( $opts->getChangedValues() ) ); |
1363 | $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) ); |
1364 | } |
1365 | |
1366 | $opts->validateIntBounds( 'limit', 0, 5000 ); |
1367 | $opts->validateBounds( 'days', 0, |
1368 | $this->getConfig()->get( MainConfigNames::RCMaxAge ) / ( 3600 * 24 ) ); |
1369 | } |
1370 | |
1371 | /** |
1372 | * Fix invalid options by resetting pairs that should never appear together. |
1373 | * |
1374 | * @param FormOptions $opts |
1375 | * @return bool True if any option was reset |
1376 | */ |
1377 | private function fixContradictoryOptions( FormOptions $opts ) { |
1378 | $fixed = $this->fixBackwardsCompatibilityOptions( $opts ); |
1379 | |
1380 | foreach ( $this->filterGroups as $filterGroup ) { |
1381 | if ( $filterGroup instanceof ChangesListBooleanFilterGroup ) { |
1382 | $filters = $filterGroup->getFilters(); |
1383 | |
1384 | if ( count( $filters ) === 1 ) { |
1385 | // legacy boolean filters should not be considered |
1386 | continue; |
1387 | } |
1388 | |
1389 | $allInGroupEnabled = array_reduce( |
1390 | $filters, |
1391 | static function ( bool $carry, ChangesListBooleanFilter $filter ) use ( $opts ) { |
1392 | return $carry && $opts[ $filter->getName() ]; |
1393 | }, |
1394 | /* initialValue */ count( $filters ) > 0 |
1395 | ); |
1396 | |
1397 | if ( $allInGroupEnabled ) { |
1398 | foreach ( $filters as $filter ) { |
1399 | $opts[ $filter->getName() ] = false; |
1400 | } |
1401 | |
1402 | $fixed = true; |
1403 | } |
1404 | } |
1405 | } |
1406 | |
1407 | return $fixed; |
1408 | } |
1409 | |
1410 | /** |
1411 | * Fix a special case (hideanons=1 and hideliu=1) in a special way, for backwards |
1412 | * compatibility. |
1413 | * |
1414 | * This is deprecated and may be removed. |
1415 | * |
1416 | * @param FormOptions $opts |
1417 | * @return bool True if this change was mode |
1418 | */ |
1419 | private function fixBackwardsCompatibilityOptions( FormOptions $opts ) { |
1420 | if ( $opts['hideanons'] && $opts['hideliu'] ) { |
1421 | $opts->reset( 'hideanons' ); |
1422 | if ( !$opts['hidebots'] ) { |
1423 | $opts->reset( 'hideliu' ); |
1424 | $opts['hidehumans'] = 1; |
1425 | } |
1426 | |
1427 | return true; |
1428 | } |
1429 | |
1430 | return false; |
1431 | } |
1432 | |
1433 | /** |
1434 | * Replace old options with their structured UI equivalents |
1435 | * |
1436 | * @param FormOptions $opts |
1437 | * @return bool True if the change was made |
1438 | */ |
1439 | public function replaceOldOptions( FormOptions $opts ) { |
1440 | if ( !$this->isStructuredFilterUiEnabled() ) { |
1441 | return false; |
1442 | } |
1443 | |
1444 | $changed = false; |
1445 | |
1446 | // At this point 'hideanons' and 'hideliu' cannot be both true, |
1447 | // because fixBackwardsCompatibilityOptions resets (at least) 'hideanons' in such case |
1448 | if ( $opts[ 'hideanons' ] ) { |
1449 | $opts->reset( 'hideanons' ); |
1450 | $opts[ 'userExpLevel' ] = 'registered'; |
1451 | $changed = true; |
1452 | } |
1453 | |
1454 | if ( $opts[ 'hideliu' ] ) { |
1455 | $opts->reset( 'hideliu' ); |
1456 | $opts[ 'userExpLevel' ] = 'unregistered'; |
1457 | $changed = true; |
1458 | } |
1459 | |
1460 | if ( $this->getFilterGroup( 'legacyReviewStatus' ) ) { |
1461 | if ( $opts[ 'hidepatrolled' ] ) { |
1462 | $opts->reset( 'hidepatrolled' ); |
1463 | $opts[ 'reviewStatus' ] = 'unpatrolled'; |
1464 | $changed = true; |
1465 | } |
1466 | |
1467 | if ( $opts[ 'hideunpatrolled' ] ) { |
1468 | $opts->reset( 'hideunpatrolled' ); |
1469 | $opts[ 'reviewStatus' ] = implode( |
1470 | ChangesListStringOptionsFilterGroup::SEPARATOR, |
1471 | [ 'manual', 'auto' ] |
1472 | ); |
1473 | $changed = true; |
1474 | } |
1475 | } |
1476 | |
1477 | return $changed; |
1478 | } |
1479 | |
1480 | /** |
1481 | * Convert parameters values from true/false to 1/0 |
1482 | * so they are not omitted by wfArrayToCgi() |
1483 | * T38524 |
1484 | * |
1485 | * @param array $params |
1486 | * @return array |
1487 | */ |
1488 | protected function convertParamsForLink( $params ) { |
1489 | foreach ( $params as &$value ) { |
1490 | if ( $value === false ) { |
1491 | $value = '0'; |
1492 | } |
1493 | } |
1494 | unset( $value ); |
1495 | return $params; |
1496 | } |
1497 | |
1498 | /** |
1499 | * Sets appropriate tables, fields, conditions, etc. depending on which filters |
1500 | * the user requested. |
1501 | * |
1502 | * @param array &$tables Array of tables; see IReadableDatabase::select $table |
1503 | * @param array &$fields Array of fields; see IReadableDatabase::select $vars |
1504 | * @param array &$conds Array of conditions; see IReadableDatabase::select $conds |
1505 | * @param array &$query_options Array of query options; see IReadableDatabase::select $options |
1506 | * @param array &$join_conds Array of join conditions; see IReadableDatabase::select $join_conds |
1507 | * @param FormOptions $opts |
1508 | */ |
1509 | protected function buildQuery( &$tables, &$fields, &$conds, &$query_options, |
1510 | &$join_conds, FormOptions $opts |
1511 | ) { |
1512 | $dbr = $this->getDB(); |
1513 | $isStructuredUI = $this->isStructuredFilterUiEnabled(); |
1514 | |
1515 | /** @var ChangesListFilterGroup $filterGroup */ |
1516 | foreach ( $this->filterGroups as $filterGroup ) { |
1517 | $filterGroup->modifyQuery( $dbr, $this, $tables, $fields, $conds, |
1518 | $query_options, $join_conds, $opts, $isStructuredUI ); |
1519 | } |
1520 | |
1521 | // Namespace filtering |
1522 | if ( $opts[ 'namespace' ] !== '' ) { |
1523 | $namespaces = explode( ';', $opts[ 'namespace' ] ); |
1524 | |
1525 | $namespaces = $this->expandSymbolicNamespaceFilters( $namespaces ); |
1526 | |
1527 | $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
1528 | $namespaces = array_filter( $namespaces, [ $namespaceInfo, 'exists' ] ); |
1529 | |
1530 | if ( $namespaces !== [] ) { |
1531 | // Namespaces are just ints, use them as int when acting with the database |
1532 | $namespaces = array_map( 'intval', $namespaces ); |
1533 | |
1534 | if ( $opts[ 'associated' ] ) { |
1535 | $associatedNamespaces = array_map( |
1536 | [ $namespaceInfo, 'getAssociated' ], |
1537 | array_filter( $namespaces, [ $namespaceInfo, 'hasTalkNamespace' ] ) |
1538 | ); |
1539 | $namespaces = array_unique( array_merge( $namespaces, $associatedNamespaces ) ); |
1540 | } |
1541 | |
1542 | $operator = $opts[ 'invert' ] ? '!=' : '='; |
1543 | sort( $namespaces ); |
1544 | $conds[] = $dbr->expr( 'rc_namespace', $operator, $namespaces ); |
1545 | } |
1546 | } |
1547 | |
1548 | // Calculate cutoff |
1549 | $cutoff_unixtime = ConvertibleTimestamp::time() - $opts['days'] * 3600 * 24; |
1550 | $cutoff = $dbr->timestamp( $cutoff_unixtime ); |
1551 | |
1552 | $fromValid = preg_match( '/^[0-9]{14}$/', $opts['from'] ); |
1553 | if ( $fromValid && $opts['from'] > wfTimestamp( TS_MW, $cutoff ) ) { |
1554 | $cutoff = $dbr->timestamp( $opts['from'] ); |
1555 | } else { |
1556 | $opts->reset( 'from' ); |
1557 | } |
1558 | |
1559 | $conds[] = $dbr->expr( 'rc_timestamp', '>=', $cutoff ); |
1560 | } |
1561 | |
1562 | /** |
1563 | * Process the query |
1564 | * |
1565 | * @param array $tables Array of tables; see IReadableDatabase::select $table |
1566 | * @param array $fields Array of fields; see IReadableDatabase::select $vars |
1567 | * @param array $conds Array of conditions; see IReadableDatabase::select $conds |
1568 | * @param array $query_options Array of query options; see IReadableDatabase::select $options |
1569 | * @param array $join_conds Array of join conditions; see IReadableDatabase::select $join_conds |
1570 | * @param FormOptions $opts |
1571 | * @return bool|IResultWrapper Result or false |
1572 | */ |
1573 | protected function doMainQuery( $tables, $fields, $conds, |
1574 | $query_options, $join_conds, FormOptions $opts |
1575 | ) { |
1576 | $rcQuery = RecentChange::getQueryInfo(); |
1577 | $tables = array_merge( $tables, $rcQuery['tables'] ); |
1578 | $fields = array_merge( $rcQuery['fields'], $fields ); |
1579 | $join_conds = array_merge( $join_conds, $rcQuery['joins'] ); |
1580 | |
1581 | MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery( |
1582 | $tables, |
1583 | $fields, |
1584 | $conds, |
1585 | $join_conds, |
1586 | $query_options, |
1587 | '', |
1588 | $opts[ 'inverttags' ] |
1589 | ); |
1590 | |
1591 | if ( |
1592 | !$this->runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts ) |
1593 | ) { |
1594 | return false; |
1595 | } |
1596 | |
1597 | $dbr = $this->getDB(); |
1598 | |
1599 | return $dbr->newSelectQueryBuilder() |
1600 | ->tables( $tables ) |
1601 | ->fields( $fields ) |
1602 | ->conds( $conds ) |
1603 | ->caller( __METHOD__ ) |
1604 | ->options( $query_options ) |
1605 | ->joinConds( $join_conds ) |
1606 | ->fetchResultSet(); |
1607 | } |
1608 | |
1609 | protected function runMainQueryHook( &$tables, &$fields, &$conds, |
1610 | &$query_options, &$join_conds, $opts |
1611 | ) { |
1612 | return $this->getHookRunner()->onChangesListSpecialPageQuery( |
1613 | $this->getName(), $tables, $fields, $conds, $query_options, $join_conds, $opts ); |
1614 | } |
1615 | |
1616 | /** |
1617 | * Which database to use for read queries |
1618 | * |
1619 | * @return IReadableDatabase |
1620 | */ |
1621 | protected function getDB(): IReadableDatabase { |
1622 | return MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
1623 | } |
1624 | |
1625 | /** |
1626 | * Send header output to the OutputPage object, only called if not using feeds |
1627 | * |
1628 | * @param int $rowCount Number of database rows |
1629 | * @param FormOptions $opts |
1630 | */ |
1631 | private function webOutputHeader( $rowCount, $opts ) { |
1632 | if ( !$this->including() ) { |
1633 | $this->outputFeedLinks(); |
1634 | $this->doHeader( $opts, $rowCount ); |
1635 | } |
1636 | } |
1637 | |
1638 | /** |
1639 | * Send output to the OutputPage object, only called if not used feeds |
1640 | * |
1641 | * @param IResultWrapper $rows Database rows |
1642 | * @param FormOptions $opts |
1643 | */ |
1644 | public function webOutput( $rows, $opts ) { |
1645 | $this->webOutputHeader( $rows->numRows(), $opts ); |
1646 | |
1647 | $this->outputChangesList( $rows, $opts ); |
1648 | } |
1649 | |
1650 | public function outputFeedLinks() { |
1651 | // nothing by default |
1652 | } |
1653 | |
1654 | /** |
1655 | * Build and output the actual changes list. |
1656 | * |
1657 | * @param IResultWrapper $rows Database rows |
1658 | * @param FormOptions $opts |
1659 | */ |
1660 | abstract public function outputChangesList( $rows, $opts ); |
1661 | |
1662 | /** |
1663 | * Set the text to be displayed above the changes |
1664 | * |
1665 | * @param FormOptions $opts |
1666 | * @param int $numRows Number of rows in the result to show after this header |
1667 | */ |
1668 | public function doHeader( $opts, $numRows ) { |
1669 | $this->setTopText( $opts ); |
1670 | |
1671 | // @todo Lots of stuff should be done here. |
1672 | |
1673 | $this->setBottomText( $opts ); |
1674 | } |
1675 | |
1676 | /** |
1677 | * Send the text to be displayed before the options. |
1678 | * Should use $this->getOutput()->addWikiTextAsInterface() |
1679 | * or similar methods to print the text. |
1680 | * |
1681 | * @param FormOptions $opts |
1682 | */ |
1683 | public function setTopText( FormOptions $opts ) { |
1684 | // nothing by default |
1685 | } |
1686 | |
1687 | /** |
1688 | * Send the text to be displayed after the options. |
1689 | * Should use $this->getOutput()->addWikiTextAsInterface() |
1690 | * or similar methods to print the text. |
1691 | * |
1692 | * @param FormOptions $opts |
1693 | */ |
1694 | public function setBottomText( FormOptions $opts ) { |
1695 | // nothing by default |
1696 | } |
1697 | |
1698 | /** |
1699 | * Get options to be displayed in a form |
1700 | * @todo This should handle options returned by getDefaultOptions(). |
1701 | * @todo Not called by anything in this class (but is in subclasses), should be |
1702 | * called by something… doHeader() maybe? |
1703 | * |
1704 | * @param FormOptions $opts |
1705 | * @return array |
1706 | */ |
1707 | public function getExtraOptions( $opts ) { |
1708 | return []; |
1709 | } |
1710 | |
1711 | /** |
1712 | * Return the legend displayed within the fieldset |
1713 | * |
1714 | * @return string |
1715 | */ |
1716 | public function makeLegend() { |
1717 | $context = $this->getContext(); |
1718 | $user = $context->getUser(); |
1719 | # The legend showing what the letters and stuff mean |
1720 | $legend = Html::openElement( 'dl' ) . "\n"; |
1721 | # Iterates through them and gets the messages for both letter and tooltip |
1722 | $legendItems = $context->getConfig()->get( MainConfigNames::RecentChangesFlags ); |
1723 | if ( !( $user->useRCPatrol() || $user->useNPPatrol() ) ) { |
1724 | unset( $legendItems['unpatrolled'] ); |
1725 | } |
1726 | foreach ( $legendItems as $key => $item ) { # generate items of the legend |
1727 | $label = $item['legend'] ?? $item['title']; |
1728 | $letter = $item['letter']; |
1729 | $cssClass = $item['class'] ?? $key; |
1730 | |
1731 | $legend .= Html::element( 'dt', |
1732 | [ 'class' => $cssClass ], $context->msg( $letter )->text() |
1733 | ) . "\n" . |
1734 | Html::rawElement( 'dd', |
1735 | [ 'class' => Sanitizer::escapeClass( 'mw-changeslist-legend-' . $key ) ], |
1736 | $context->msg( $label )->parse() |
1737 | ) . "\n"; |
1738 | } |
1739 | # (+-123) |
1740 | $legend .= Html::rawElement( 'dt', |
1741 | [ 'class' => 'mw-plusminus-pos' ], |
1742 | $context->msg( 'recentchanges-legend-plusminus' )->parse() |
1743 | ) . "\n"; |
1744 | $legend .= Html::element( |
1745 | 'dd', |
1746 | [ 'class' => 'mw-changeslist-legend-plusminus' ], |
1747 | $context->msg( 'recentchanges-label-plusminus' )->text() |
1748 | ) . "\n"; |
1749 | // Watchlist expiry clock icon. |
1750 | if ( $context->getConfig()->get( MainConfigNames::WatchlistExpiry ) && !$this->including() ) { |
1751 | $widget = new IconWidget( [ |
1752 | 'icon' => 'clock', |
1753 | 'classes' => [ 'mw-changesList-watchlistExpiry' ], |
1754 | ] ); |
1755 | // Link the image to its label for assistive technologies. |
1756 | $watchlistLabelId = 'mw-changeslist-watchlistExpiry-label'; |
1757 | $widget->getIconElement()->setAttributes( [ |
1758 | 'role' => 'img', |
1759 | 'aria-labelledby' => $watchlistLabelId, |
1760 | ] ); |
1761 | $legend .= Html::rawElement( |
1762 | 'dt', |
1763 | [ 'class' => 'mw-changeslist-legend-watchlistexpiry' ], |
1764 | $widget |
1765 | ); |
1766 | $legend .= Html::element( |
1767 | 'dd', |
1768 | [ 'class' => 'mw-changeslist-legend-watchlistexpiry', 'id' => $watchlistLabelId ], |
1769 | $context->msg( 'recentchanges-legend-watchlistexpiry' )->text() |
1770 | ); |
1771 | } |
1772 | $legend .= Html::closeElement( 'dl' ) . "\n"; |
1773 | |
1774 | $legendHeading = $this->isStructuredFilterUiEnabled() ? |
1775 | $context->msg( 'rcfilters-legend-heading' )->parse() : |
1776 | $context->msg( 'recentchanges-legend-heading' )->parse(); |
1777 | |
1778 | # Collapsible |
1779 | $collapsedState = $this->getRequest()->getCookie( 'changeslist-state' ); |
1780 | |
1781 | $legend = Html::rawElement( 'details', [ |
1782 | 'class' => 'mw-changeslist-legend', |
1783 | 'open' => $collapsedState !== 'collapsed' ? 'open' : null, |
1784 | ], |
1785 | Html::rawElement( 'summary', [], $legendHeading ) . |
1786 | $legend |
1787 | ); |
1788 | |
1789 | return $legend; |
1790 | } |
1791 | |
1792 | /** |
1793 | * Add page-specific modules. |
1794 | */ |
1795 | protected function addModules() { |
1796 | $out = $this->getOutput(); |
1797 | // Styles and behavior for the legend box (see makeLegend()) |
1798 | $out->addModuleStyles( [ |
1799 | 'mediawiki.interface.helpers.styles', |
1800 | 'mediawiki.special.changeslist.legend', |
1801 | 'mediawiki.special.changeslist', |
1802 | ] ); |
1803 | $out->addModules( 'mediawiki.special.changeslist.legend.js' ); |
1804 | |
1805 | if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) { |
1806 | $out->addModules( 'mediawiki.rcfilters.filters.ui' ); |
1807 | $out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' ); |
1808 | } |
1809 | } |
1810 | |
1811 | protected function getGroupName() { |
1812 | return 'changes'; |
1813 | } |
1814 | |
1815 | /** |
1816 | * Return expression that is true when the user is or isn't registered. |
1817 | * @param bool $isRegistered |
1818 | * @param IReadableDatabase $dbr |
1819 | * @return IExpression |
1820 | */ |
1821 | private function getRegisteredExpr( $isRegistered, $dbr ): IExpression { |
1822 | $expr = $dbr->expr( 'actor_user', $isRegistered ? '!=' : '=', null ); |
1823 | if ( !$this->tempUserConfig->isKnown() ) { |
1824 | return $expr; |
1825 | } |
1826 | if ( $isRegistered ) { |
1827 | return $expr->andExpr( $this->tempUserConfig->getMatchCondition( $dbr, |
1828 | 'actor_name', IExpression::NOT_LIKE ) ); |
1829 | } else { |
1830 | return $expr->orExpr( $this->tempUserConfig->getMatchCondition( $dbr, |
1831 | 'actor_name', IExpression::LIKE ) ); |
1832 | } |
1833 | } |
1834 | |
1835 | /** |
1836 | * Return expression that is true when the user has reached the given experience level. |
1837 | * @param string $level 'learner' or 'experienced' |
1838 | * @param int $now Current time as UNIX timestamp (if 0, uses actual time) |
1839 | * @param IReadableDatabase $dbr |
1840 | * @param bool $asNotCondition |
1841 | * @return IExpression |
1842 | */ |
1843 | private function getExperienceExpr( $level, $now, IReadableDatabase $dbr, $asNotCondition = false ): IExpression { |
1844 | $config = $this->getConfig(); |
1845 | |
1846 | $configSince = [ |
1847 | 'learner' => $config->get( MainConfigNames::LearnerMemberSince ), |
1848 | 'experienced' => $config->get( MainConfigNames::ExperiencedUserMemberSince ), |
1849 | ][$level]; |
1850 | if ( $now === 0 ) { |
1851 | $now = ConvertibleTimestamp::time(); |
1852 | } |
1853 | $secondsPerDay = 86400; |
1854 | $timeCutoff = $now - $configSince * $secondsPerDay; |
1855 | |
1856 | $editCutoff = [ |
1857 | 'learner' => $config->get( MainConfigNames::LearnerEdits ), |
1858 | 'experienced' => $config->get( MainConfigNames::ExperiencedUserEdits ), |
1859 | ][$level]; |
1860 | |
1861 | if ( $asNotCondition ) { |
1862 | return $dbr->expr( 'user_editcount', '<', intval( $editCutoff ) ) |
1863 | ->or( 'user_registration', '>', $dbr->timestamp( $timeCutoff ) ); |
1864 | } |
1865 | return $dbr->expr( 'user_editcount', '>=', intval( $editCutoff ) )->andExpr( |
1866 | // Users who don't have user_registration set are very old, so we assume they're above any cutoff |
1867 | $dbr->expr( 'user_registration', '=', null ) |
1868 | ->or( 'user_registration', '<=', $dbr->timestamp( $timeCutoff ) ) |
1869 | ); |
1870 | } |
1871 | |
1872 | /** |
1873 | * Filter on users' experience levels; this will not be called if nothing is |
1874 | * selected. |
1875 | * |
1876 | * @param string $specialPageClassName Class name of current special page |
1877 | * @param IContextSource $context Context, for e.g. user |
1878 | * @param IReadableDatabase $dbr Database, for addQuotes, makeList, and similar |
1879 | * @param array &$tables Array of tables; see IReadableDatabase::select $table |
1880 | * @param array &$fields Array of fields; see IReadableDatabase::select $vars |
1881 | * @param array &$conds Array of conditions; see IReadableDatabase::select $conds |
1882 | * @param array &$query_options Array of query options; see IReadableDatabase::select $options |
1883 | * @param array &$join_conds Array of join conditions; see IReadableDatabase::select $join_conds |
1884 | * @param array $selectedExpLevels The allowed active values, sorted |
1885 | * @param int $now Current time as UNIX timestamp (if 0, uses actual time) |
1886 | */ |
1887 | public function filterOnUserExperienceLevel( $specialPageClassName, $context, $dbr, |
1888 | &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedExpLevels, $now = 0 |
1889 | ) { |
1890 | $selected = array_fill_keys( $selectedExpLevels, true ); |
1891 | |
1892 | $isUnregistered = $this->getRegisteredExpr( false, $dbr ); |
1893 | $isRegistered = $this->getRegisteredExpr( true, $dbr ); |
1894 | $aboveNewcomer = $this->getExperienceExpr( 'learner', $now, $dbr ); |
1895 | $notAboveNewcomer = $this->getExperienceExpr( 'learner', $now, $dbr, true ); |
1896 | $aboveLearner = $this->getExperienceExpr( 'experienced', $now, $dbr ); |
1897 | $notAboveLearner = $this->getExperienceExpr( 'experienced', $now, $dbr, true ); |
1898 | |
1899 | // We need to select some range of user experience levels, from the following table: |
1900 | // | Unregistered | --------- Registered --------- | |
1901 | // | | Newcomers | Learners | Experienced | |
1902 | // |<------------>|<----------->|<---------->|<----------->| |
1903 | // We just need to define a condition for each of the columns, figure out which are selected, |
1904 | // and then OR them together. |
1905 | $columnConds = [ |
1906 | 'unregistered' => $isUnregistered, |
1907 | 'registered' => $isRegistered, |
1908 | 'newcomer' => $dbr->andExpr( [ $isRegistered, $notAboveNewcomer ] ), |
1909 | 'learner' => $dbr->andExpr( [ $isRegistered, $aboveNewcomer, $notAboveLearner ] ), |
1910 | 'experienced' => $dbr->andExpr( [ $isRegistered, $aboveLearner ] ), |
1911 | ]; |
1912 | |
1913 | // There are some cases where we can easily optimize away some queries: |
1914 | // | Unregistered | --------- Registered --------- | |
1915 | // | | Newcomers | Learners | Experienced | |
1916 | // | |<-------------------------------------->| (1) |
1917 | // |<----------------------------------------------------->| (2) |
1918 | |
1919 | // (1) Selecting all of "Newcomers", "Learners" and "Experienced users" is the same as "Registered". |
1920 | if ( |
1921 | isset( $selected['registered'] ) || |
1922 | ( isset( $selected['newcomer'] ) && isset( $selected['learner'] ) && isset( $selected['experienced'] ) ) |
1923 | ) { |
1924 | unset( $selected['newcomer'], $selected['learner'], $selected['experienced'] ); |
1925 | $selected['registered'] = true; |
1926 | } |
1927 | // (2) Selecting "Unregistered" and "Registered" covers all users. |
1928 | if ( isset( $selected['registered'] ) && isset( $selected['unregistered'] ) ) { |
1929 | unset( $selected['registered'], $selected['unregistered'] ); |
1930 | } |
1931 | |
1932 | // Combine the conditions for the selected columns. |
1933 | if ( !$selected ) { |
1934 | return; |
1935 | } |
1936 | $selectedColumnConds = array_values( array_intersect_key( $columnConds, $selected ) ); |
1937 | $conds[] = $dbr->orExpr( $selectedColumnConds ); |
1938 | |
1939 | // Add necessary tables to the queries. |
1940 | $join_conds['recentchanges_actor'] = [ 'JOIN', 'actor_id=rc_actor' ]; |
1941 | if ( isset( $selected['newcomer'] ) || isset( $selected['learner'] ) || isset( $selected['experienced'] ) ) { |
1942 | $tables[] = 'user'; |
1943 | $join_conds['user'] = [ 'LEFT JOIN', 'actor_user=user_id' ]; |
1944 | } |
1945 | } |
1946 | |
1947 | /** |
1948 | * Check whether the structured filter UI is enabled |
1949 | * |
1950 | * @return bool |
1951 | */ |
1952 | public function isStructuredFilterUiEnabled() { |
1953 | if ( $this->getRequest()->getBool( 'rcfilters' ) ) { |
1954 | return true; |
1955 | } |
1956 | |
1957 | return static::checkStructuredFilterUiEnabled( $this->getUser() ); |
1958 | } |
1959 | |
1960 | /** |
1961 | * Static method to check whether StructuredFilter UI is enabled for the given user |
1962 | * |
1963 | * @since 1.31 |
1964 | * @param UserIdentity $user |
1965 | * @return bool |
1966 | */ |
1967 | public static function checkStructuredFilterUiEnabled( UserIdentity $user ) { |
1968 | return !MediaWikiServices::getInstance() |
1969 | ->getUserOptionsLookup() |
1970 | ->getOption( $user, 'rcenhancedfilters-disable' ); |
1971 | } |
1972 | |
1973 | /** |
1974 | * Get the default value of the number of changes to display when loading |
1975 | * the result set. |
1976 | * |
1977 | * @since 1.30 |
1978 | * @return int |
1979 | */ |
1980 | public function getDefaultLimit() { |
1981 | return MediaWikiServices::getInstance() |
1982 | ->getUserOptionsLookup() |
1983 | ->getIntOption( $this->getUser(), $this->getLimitPreferenceName() ); |
1984 | } |
1985 | |
1986 | /** |
1987 | * Get the default value of the number of days to display when loading |
1988 | * the result set. |
1989 | * Supports fractional values, and should be cast to a float. |
1990 | * |
1991 | * @since 1.30 |
1992 | * @return float |
1993 | */ |
1994 | public function getDefaultDays() { |
1995 | return floatval( MediaWikiServices::getInstance() |
1996 | ->getUserOptionsLookup() |
1997 | ->getOption( $this->getUser(), $this->getDefaultDaysPreferenceName() ) ); |
1998 | } |
1999 | |
2000 | /** |
2001 | * Getting the preference name for 'limit'. |
2002 | * |
2003 | * @since 1.37 |
2004 | * @return string |
2005 | */ |
2006 | abstract protected function getLimitPreferenceName(): string; |
2007 | |
2008 | /** |
2009 | * Preference name for saved queries. |
2010 | * |
2011 | * @since 1.38 |
2012 | * @return string |
2013 | */ |
2014 | abstract protected function getSavedQueriesPreferenceName(): string; |
2015 | |
2016 | /** |
2017 | * Preference name for 'days'. |
2018 | * |
2019 | * @since 1.38 |
2020 | * @return string |
2021 | */ |
2022 | abstract protected function getDefaultDaysPreferenceName(): string; |
2023 | |
2024 | /** |
2025 | * Preference name for collapsing the active filter display. |
2026 | * |
2027 | * @since 1.38 |
2028 | * @return string |
2029 | */ |
2030 | abstract protected function getCollapsedPreferenceName(): string; |
2031 | |
2032 | /** |
2033 | * @param array $namespaces |
2034 | * @return array |
2035 | */ |
2036 | private function expandSymbolicNamespaceFilters( array $namespaces ) { |
2037 | $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
2038 | $symbolicFilters = [ |
2039 | 'all-contents' => $nsInfo->getSubjectNamespaces(), |
2040 | 'all-discussions' => $nsInfo->getTalkNamespaces(), |
2041 | ]; |
2042 | $additionalNamespaces = []; |
2043 | foreach ( $symbolicFilters as $name => $values ) { |
2044 | if ( in_array( $name, $namespaces ) ) { |
2045 | $additionalNamespaces = array_merge( $additionalNamespaces, $values ); |
2046 | } |
2047 | } |
2048 | $namespaces = array_diff( $namespaces, array_keys( $symbolicFilters ) ); |
2049 | $namespaces = array_merge( $namespaces, $additionalNamespaces ); |
2050 | return array_unique( $namespaces ); |
2051 | } |
2052 | } |
2053 | |
2054 | /** @deprecated class alias since 1.41 */ |
2055 | class_alias( ChangesListSpecialPage::class, 'ChangesListSpecialPage' ); |