Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.38% |
227 / 238 |
|
80.00% |
16 / 20 |
CRAP | |
0.00% |
0 / 1 |
RestrictionStore | |
95.38% |
227 / 238 |
|
80.00% |
16 / 20 |
79 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
getRestrictions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getAllRestrictions | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getRestrictionExpiry | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getCreateProtection | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
deleteCreateProtection | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
isSemiProtected | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
5 | |||
isProtected | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
6 | |||
isCascadeProtected | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
1.12 | |||
listApplicableRestrictionTypes | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
listAllRestrictionTypes | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
loadRestrictions | |
96.08% |
49 / 51 |
|
0.00% |
0 / 1 |
13 | |||
loadRestrictionsFromRows | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
9 | |||
getCreateProtectionInternal | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
6 | |||
getCascadeProtectionSources | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCascadeProtectionSourcesInternal | |
84.78% |
39 / 46 |
|
0.00% |
0 / 1 |
15.79 | |||
areRestrictionsLoaded | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
areCascadeProtectionSourcesLoaded | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
1.12 | |||
areRestrictionsCascading | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
flushRestrictions | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Permissions; |
4 | |
5 | use DBAccessObjectUtils; |
6 | use IDBAccessObject; |
7 | use MediaWiki\Cache\CacheKeyHelper; |
8 | use MediaWiki\Cache\LinkCache; |
9 | use MediaWiki\CommentStore\CommentStore; |
10 | use MediaWiki\Config\ServiceOptions; |
11 | use MediaWiki\HookContainer\HookContainer; |
12 | use MediaWiki\HookContainer\HookRunner; |
13 | use MediaWiki\Linker\LinksMigration; |
14 | use MediaWiki\MainConfigNames; |
15 | use MediaWiki\Page\PageIdentity; |
16 | use MediaWiki\Page\PageIdentityValue; |
17 | use MediaWiki\Page\PageStore; |
18 | use MediaWiki\Title\Title; |
19 | use MediaWiki\Title\TitleValue; |
20 | use stdClass; |
21 | use WANObjectCache; |
22 | use Wikimedia\Rdbms\Database; |
23 | use Wikimedia\Rdbms\ILoadBalancer; |
24 | use Wikimedia\Rdbms\IReadableDatabase; |
25 | |
26 | /** |
27 | * Class RestrictionStore |
28 | * |
29 | * @since 1.37 |
30 | */ |
31 | class RestrictionStore { |
32 | |
33 | /** @internal */ |
34 | public const CONSTRUCTOR_OPTIONS = [ |
35 | MainConfigNames::NamespaceProtection, |
36 | MainConfigNames::RestrictionLevels, |
37 | MainConfigNames::RestrictionTypes, |
38 | MainConfigNames::SemiprotectedRestrictionLevels, |
39 | ]; |
40 | |
41 | /** @var ServiceOptions */ |
42 | private $options; |
43 | |
44 | /** @var WANObjectCache */ |
45 | private $wanCache; |
46 | |
47 | /** @var ILoadBalancer */ |
48 | private $loadBalancer; |
49 | |
50 | /** @var LinkCache */ |
51 | private $linkCache; |
52 | |
53 | /** @var LinksMigration */ |
54 | private $linksMigration; |
55 | |
56 | /** @var CommentStore */ |
57 | private $commentStore; |
58 | |
59 | /** @var HookContainer */ |
60 | private $hookContainer; |
61 | |
62 | /** @var HookRunner */ |
63 | private $hookRunner; |
64 | |
65 | /** @var PageStore */ |
66 | private $pageStore; |
67 | |
68 | /** |
69 | * @var array[] Caching various restrictions data in the following format: |
70 | * cache key => [ |
71 | * string[] `restrictions` => restrictions loaded for pages |
72 | * ?string `expiry` => restrictions expiry data for pages |
73 | * ?array `create_protection` => value for getCreateProtection |
74 | * bool `cascade` => cascade restrictions on this page to included templates and images? |
75 | * array[] `cascade_sources` => the results of getCascadeProtectionSources |
76 | * bool `has_cascading` => Are cascading restrictions in effect on this page? |
77 | * ] |
78 | */ |
79 | private $cache = []; |
80 | |
81 | /** |
82 | * @param ServiceOptions $options |
83 | * @param WANObjectCache $wanCache |
84 | * @param ILoadBalancer $loadBalancer |
85 | * @param LinkCache $linkCache |
86 | * @param LinksMigration $linksMigration |
87 | * @param CommentStore $commentStore |
88 | * @param HookContainer $hookContainer |
89 | * @param PageStore $pageStore |
90 | */ |
91 | public function __construct( |
92 | ServiceOptions $options, |
93 | WANObjectCache $wanCache, |
94 | ILoadBalancer $loadBalancer, |
95 | LinkCache $linkCache, |
96 | LinksMigration $linksMigration, |
97 | CommentStore $commentStore, |
98 | HookContainer $hookContainer, |
99 | PageStore $pageStore |
100 | ) { |
101 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
102 | $this->options = $options; |
103 | $this->wanCache = $wanCache; |
104 | $this->loadBalancer = $loadBalancer; |
105 | $this->linkCache = $linkCache; |
106 | $this->linksMigration = $linksMigration; |
107 | $this->commentStore = $commentStore; |
108 | $this->hookContainer = $hookContainer; |
109 | $this->hookRunner = new HookRunner( $hookContainer ); |
110 | $this->pageStore = $pageStore; |
111 | } |
112 | |
113 | /** |
114 | * Returns list of restrictions for specified page |
115 | * |
116 | * @param PageIdentity $page Must be local |
117 | * @param string $action Action that restrictions need to be checked for |
118 | * @return string[] Restriction levels needed to take the action. All levels are required. Note |
119 | * that restriction levels are normally user rights, but 'sysop' and 'autoconfirmed' are also |
120 | * allowed for backwards compatibility. These should be mapped to 'editprotected' and |
121 | * 'editsemiprotected' respectively. Returns an empty array if there are no restrictions set |
122 | * for this action (including for unrecognized actions). |
123 | */ |
124 | public function getRestrictions( PageIdentity $page, string $action ): array { |
125 | $page->assertWiki( PageIdentity::LOCAL ); |
126 | |
127 | // Optimization: Avoid repeatedly fetching page restrictions (from cache or DB) |
128 | // for repeated PermissionManager::userCan calls, if this action cannot be restricted |
129 | // in the first place. This is primarily to improve batch rendering on RecentChanges, |
130 | // where as of writing this will save 0.5s on a 8.0s response. (T341319) |
131 | $restrictionTypes = $this->listApplicableRestrictionTypes( $page ); |
132 | if ( !in_array( $action, $restrictionTypes ) ) { |
133 | return []; |
134 | } |
135 | |
136 | $restrictions = $this->getAllRestrictions( $page ); |
137 | return $restrictions[$action] ?? []; |
138 | } |
139 | |
140 | /** |
141 | * Returns the restricted actions and their restrictions for the specified page |
142 | * |
143 | * @param PageIdentity $page Must be local |
144 | * @return string[][] Keys are actions, values are arrays as returned by |
145 | * RestrictionStore::getRestrictions(). Empty if no restrictions are in place. |
146 | */ |
147 | public function getAllRestrictions( PageIdentity $page ): array { |
148 | $page->assertWiki( PageIdentity::LOCAL ); |
149 | |
150 | if ( !$this->areRestrictionsLoaded( $page ) ) { |
151 | $this->loadRestrictions( $page ); |
152 | } |
153 | return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ?? []; |
154 | } |
155 | |
156 | /** |
157 | * Get the expiry time for the restriction against a given action |
158 | * |
159 | * @param PageIdentity $page Must be local |
160 | * @param string $action |
161 | * @return ?string 14-char timestamp, or 'infinity' if the page is protected forever or not |
162 | * protected at all, or null if the action is not recognized. |
163 | */ |
164 | public function getRestrictionExpiry( PageIdentity $page, string $action ): ?string { |
165 | $page->assertWiki( PageIdentity::LOCAL ); |
166 | |
167 | if ( !$this->areRestrictionsLoaded( $page ) ) { |
168 | $this->loadRestrictions( $page ); |
169 | } |
170 | return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['expiry'][$action] ?? null; |
171 | } |
172 | |
173 | /** |
174 | * Is this title subject to protection against creation? |
175 | * |
176 | * @param PageIdentity $page Must be local |
177 | * @return ?array Null if no restrictions. Otherwise an array with the following keys: |
178 | * - user: user id |
179 | * - expiry: 14-digit timestamp or 'infinity' |
180 | * - permission: string (pt_create_perm) |
181 | * - reason: string |
182 | * @internal Only to be called by Title::getTitleProtection. When that is discontinued, this |
183 | * will be too, in favor of getRestrictions( $page, 'create' ). If someone wants to know who |
184 | * protected it or the reason, there should be a method that exposes that for all restriction |
185 | * types. |
186 | */ |
187 | public function getCreateProtection( PageIdentity $page ): ?array { |
188 | $page->assertWiki( PageIdentity::LOCAL ); |
189 | |
190 | $protection = $this->getCreateProtectionInternal( $page ); |
191 | // TODO: the remapping below probably need to be migrated into other method one day |
192 | if ( $protection ) { |
193 | if ( $protection['permission'] == 'sysop' ) { |
194 | $protection['permission'] = 'editprotected'; // B/C |
195 | } |
196 | if ( $protection['permission'] == 'autoconfirmed' ) { |
197 | $protection['permission'] = 'editsemiprotected'; // B/C |
198 | } |
199 | } |
200 | return $protection; |
201 | } |
202 | |
203 | /** |
204 | * Remove any title creation protection due to page existing |
205 | * |
206 | * @param PageIdentity $page Must be local |
207 | * @internal Only to be called by WikiPage::onArticleCreate. |
208 | */ |
209 | public function deleteCreateProtection( PageIdentity $page ): void { |
210 | $page->assertWiki( PageIdentity::LOCAL ); |
211 | |
212 | $dbw = $this->loadBalancer->getConnection( DB_PRIMARY ); |
213 | $dbw->newDeleteQueryBuilder() |
214 | ->deleteFrom( 'protected_titles' ) |
215 | ->where( [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ] ) |
216 | ->caller( __METHOD__ )->execute(); |
217 | $this->cache[CacheKeyHelper::getKeyForPage( $page )]['create_protection'] = null; |
218 | } |
219 | |
220 | /** |
221 | * Is this page "semi-protected" - the *only* protection levels are listed in |
222 | * $wgSemiprotectedRestrictionLevels? |
223 | * |
224 | * @param PageIdentity $page Must be local |
225 | * @param string $action Action to check (default: edit) |
226 | * @return bool |
227 | */ |
228 | public function isSemiProtected( PageIdentity $page, string $action = 'edit' ): bool { |
229 | $page->assertWiki( PageIdentity::LOCAL ); |
230 | |
231 | $restrictions = $this->getRestrictions( $page, $action ); |
232 | $semi = $this->options->get( MainConfigNames::SemiprotectedRestrictionLevels ); |
233 | if ( !$restrictions || !$semi ) { |
234 | // Not protected, or all protection is full protection |
235 | return false; |
236 | } |
237 | |
238 | // Remap autoconfirmed to editsemiprotected for BC |
239 | foreach ( array_keys( $semi, 'editsemiprotected' ) as $key ) { |
240 | $semi[$key] = 'autoconfirmed'; |
241 | } |
242 | foreach ( array_keys( $restrictions, 'editsemiprotected' ) as $key ) { |
243 | $restrictions[$key] = 'autoconfirmed'; |
244 | } |
245 | |
246 | return !array_diff( $restrictions, $semi ); |
247 | } |
248 | |
249 | /** |
250 | * Does the title correspond to a protected article? |
251 | * |
252 | * @param PageIdentity $page Must be local |
253 | * @param string $action The action the page is protected from, by default checks all actions. |
254 | * @return bool |
255 | */ |
256 | public function isProtected( PageIdentity $page, string $action = '' ): bool { |
257 | $page->assertWiki( PageIdentity::LOCAL ); |
258 | |
259 | // Special pages have inherent protection (TODO: remove after switch to ProperPageIdentity) |
260 | if ( $page->getNamespace() === NS_SPECIAL ) { |
261 | return true; |
262 | } |
263 | |
264 | // Check regular protection levels |
265 | $applicableTypes = $this->listApplicableRestrictionTypes( $page ); |
266 | |
267 | if ( $action === '' ) { |
268 | foreach ( $applicableTypes as $type ) { |
269 | if ( $this->isProtected( $page, $type ) ) { |
270 | return true; |
271 | } |
272 | } |
273 | return false; |
274 | } |
275 | |
276 | if ( !in_array( $action, $applicableTypes ) ) { |
277 | return false; |
278 | } |
279 | |
280 | return (bool)array_diff( |
281 | array_intersect( |
282 | $this->getRestrictions( $page, $action ), |
283 | $this->options->get( MainConfigNames::RestrictionLevels ) |
284 | ), |
285 | [ '' ] |
286 | ); |
287 | } |
288 | |
289 | /** |
290 | * Cascading protection: Return true if cascading restrictions apply to this page, false if not. |
291 | * |
292 | * @param PageIdentity $page Must be local |
293 | * @return bool If the page is subject to cascading restrictions. |
294 | */ |
295 | public function isCascadeProtected( PageIdentity $page ): bool { |
296 | $page->assertWiki( PageIdentity::LOCAL ); |
297 | |
298 | return $this->getCascadeProtectionSourcesInternal( $page, true ); |
299 | } |
300 | |
301 | /** |
302 | * Returns restriction types for the current page |
303 | * |
304 | * @param PageIdentity $page Must be local |
305 | * @return string[] Applicable restriction types |
306 | */ |
307 | public function listApplicableRestrictionTypes( PageIdentity $page ): array { |
308 | $page->assertWiki( PageIdentity::LOCAL ); |
309 | |
310 | if ( !$page->canExist() ) { |
311 | return []; |
312 | } |
313 | |
314 | $types = $this->listAllRestrictionTypes( $page->exists() ); |
315 | |
316 | if ( $page->getNamespace() !== NS_FILE ) { |
317 | // Remove the upload restriction for non-file titles |
318 | $types = array_values( array_diff( $types, [ 'upload' ] ) ); |
319 | } |
320 | |
321 | if ( $this->hookContainer->isRegistered( 'TitleGetRestrictionTypes' ) ) { |
322 | $this->hookRunner->onTitleGetRestrictionTypes( |
323 | Title::newFromPageIdentity( $page ), $types ); |
324 | } |
325 | |
326 | return $types; |
327 | } |
328 | |
329 | /** |
330 | * Get a filtered list of all restriction types supported by this wiki. |
331 | * |
332 | * @param bool $exists True to get all restriction types that apply to titles that do exist, |
333 | * false for all restriction types that apply to titles that do not exist |
334 | * @return string[] |
335 | */ |
336 | public function listAllRestrictionTypes( bool $exists = true ): array { |
337 | $types = $this->options->get( MainConfigNames::RestrictionTypes ); |
338 | if ( $exists ) { |
339 | // Remove the create restriction for existing titles |
340 | return array_values( array_diff( $types, [ 'create' ] ) ); |
341 | } |
342 | |
343 | // Only the create restrictions apply to non-existing titles |
344 | return array_values( array_intersect( $types, [ 'create' ] ) ); |
345 | } |
346 | |
347 | /** |
348 | * Load restrictions from page.page_restrictions and the page_restrictions table |
349 | * |
350 | * @param PageIdentity $page Must be local |
351 | * @param int $flags IDBAccessObject::READ_XXX constants (e.g., READ_LATEST to read from |
352 | * primary DB) |
353 | * @internal Public for use in WikiPage only |
354 | */ |
355 | public function loadRestrictions( |
356 | PageIdentity $page, int $flags = IDBAccessObject::READ_NORMAL |
357 | ): void { |
358 | $page->assertWiki( PageIdentity::LOCAL ); |
359 | |
360 | if ( !$page->canExist() ) { |
361 | return; |
362 | } |
363 | |
364 | $readLatest = DBAccessObjectUtils::hasFlags( $flags, IDBAccessObject::READ_LATEST ); |
365 | |
366 | if ( $this->areRestrictionsLoaded( $page ) && !$readLatest ) { |
367 | return; |
368 | } |
369 | |
370 | $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; |
371 | |
372 | $cacheEntry['restrictions'] = []; |
373 | |
374 | // XXX Work around https://phabricator.wikimedia.org/T287575 |
375 | if ( $readLatest ) { |
376 | $page = $this->pageStore->getPageByReference( $page, $flags ) ?? $page; |
377 | } |
378 | $id = $page->getId(); |
379 | if ( $id ) { |
380 | $fname = __METHOD__; |
381 | $loadRestrictionsFromDb = static function ( IReadableDatabase $dbr ) use ( $fname, $id ) { |
382 | return iterator_to_array( |
383 | $dbr->newSelectQueryBuilder() |
384 | ->select( [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ] ) |
385 | ->from( 'page_restrictions' ) |
386 | ->where( [ 'pr_page' => $id ] ) |
387 | ->caller( $fname )->fetchResultSet() |
388 | ); |
389 | }; |
390 | |
391 | if ( $readLatest ) { |
392 | $dbr = $this->loadBalancer->getConnection( DB_PRIMARY ); |
393 | $rows = $loadRestrictionsFromDb( $dbr ); |
394 | } else { |
395 | $this->linkCache->addLinkObj( $page ); |
396 | $latestRev = $this->linkCache->getGoodLinkFieldObj( $page, 'revision' ); |
397 | if ( !$latestRev ) { |
398 | // This method can get called in the middle of page creation |
399 | // (WikiPage::doUserEditContent) where a page might have an |
400 | // id but no revisions, while checking the "autopatrol" permission. |
401 | $rows = []; |
402 | } else { |
403 | $rows = $this->wanCache->getWithSetCallback( |
404 | // Page protections always leave a new null revision |
405 | $this->wanCache->makeKey( 'page-restrictions', 'v1', $id, $latestRev ), |
406 | $this->wanCache::TTL_DAY, |
407 | function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) { |
408 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
409 | $setOpts += Database::getCacheSetOptions( $dbr ); |
410 | if ( $this->loadBalancer->hasOrMadeRecentPrimaryChanges() ) { |
411 | // TODO: cleanup Title cache and caller assumption mess in general |
412 | $ttl = WANObjectCache::TTL_UNCACHEABLE; |
413 | } |
414 | |
415 | return $loadRestrictionsFromDb( $dbr ); |
416 | } |
417 | ); |
418 | } |
419 | } |
420 | |
421 | $this->loadRestrictionsFromRows( $page, $rows ); |
422 | } else { |
423 | $titleProtection = $this->getCreateProtectionInternal( $page ); |
424 | |
425 | if ( $titleProtection ) { |
426 | $now = wfTimestampNow(); |
427 | $expiry = $titleProtection['expiry']; |
428 | |
429 | if ( !$expiry || $expiry > $now ) { |
430 | // Apply the restrictions |
431 | $cacheEntry['expiry']['create'] = $expiry ?: null; |
432 | $cacheEntry['restrictions']['create'] = |
433 | explode( ',', trim( $titleProtection['permission'] ) ); |
434 | } else { |
435 | // Get rid of the old restrictions |
436 | $cacheEntry['create_protection'] = null; |
437 | } |
438 | } else { |
439 | $cacheEntry['expiry']['create'] = 'infinity'; |
440 | } |
441 | } |
442 | } |
443 | |
444 | /** |
445 | * Compiles list of active page restrictions for this existing page. |
446 | * Public for usage by LiquidThreads. |
447 | * |
448 | * @param PageIdentity $page Must be local |
449 | * @param stdClass[] $rows Array of db result objects |
450 | */ |
451 | public function loadRestrictionsFromRows( |
452 | PageIdentity $page, array $rows |
453 | ): void { |
454 | $page->assertWiki( PageIdentity::LOCAL ); |
455 | |
456 | $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; |
457 | |
458 | $restrictionTypes = $this->listApplicableRestrictionTypes( $page ); |
459 | |
460 | foreach ( $restrictionTypes as $type ) { |
461 | $cacheEntry['restrictions'][$type] = []; |
462 | $cacheEntry['expiry'][$type] = 'infinity'; |
463 | } |
464 | |
465 | $cacheEntry['cascade'] = false; |
466 | |
467 | if ( !$rows ) { |
468 | return; |
469 | } |
470 | |
471 | // New restriction format -- load second to make them override old-style restrictions. |
472 | $now = wfTimestampNow(); |
473 | |
474 | // Cycle through all the restrictions. |
475 | foreach ( $rows as $row ) { |
476 | // Don't take care of restrictions types that aren't allowed |
477 | if ( !in_array( $row->pr_type, $restrictionTypes ) ) { |
478 | continue; |
479 | } |
480 | |
481 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
482 | $expiry = $dbr->decodeExpiry( $row->pr_expiry ); |
483 | |
484 | // Only apply the restrictions if they haven't expired! |
485 | // XXX Why would !$expiry ever be true? It should always be either 'infinity' or a |
486 | // string consisting of 14 digits. Likewise for the ?: below. |
487 | if ( !$expiry || $expiry > $now ) { |
488 | $cacheEntry['expiry'][$row->pr_type] = $expiry ?: null; |
489 | $cacheEntry['restrictions'][$row->pr_type] |
490 | = explode( ',', trim( $row->pr_level ) ); |
491 | if ( $row->pr_cascade ) { |
492 | $cacheEntry['cascade'] = true; |
493 | } |
494 | } |
495 | } |
496 | } |
497 | |
498 | /** |
499 | * Fetch title protection settings |
500 | * |
501 | * To work correctly, $this->loadRestrictions() needs to have access to the actual protections |
502 | * in the database without munging 'sysop' => 'editprotected' and 'autoconfirmed' => |
503 | * 'editsemiprotected'. |
504 | * |
505 | * @param PageIdentity $page Must be local |
506 | * @return ?array Same format as getCreateProtection(). |
507 | */ |
508 | private function getCreateProtectionInternal( PageIdentity $page ): ?array { |
509 | // Can't protect pages in special namespaces |
510 | if ( !$page->canExist() ) { |
511 | return null; |
512 | } |
513 | |
514 | // Can't apply this type of protection to pages that exist. |
515 | if ( $page->exists() ) { |
516 | return null; |
517 | } |
518 | |
519 | $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; |
520 | |
521 | if ( !$cacheEntry || !array_key_exists( 'create_protection', $cacheEntry ) ) { |
522 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
523 | $commentQuery = $this->commentStore->getJoin( 'pt_reason' ); |
524 | $row = $dbr->selectRow( |
525 | [ 'protected_titles' ] + $commentQuery['tables'], |
526 | [ 'pt_user', 'pt_expiry', 'pt_create_perm' ] + $commentQuery['fields'], |
527 | [ 'pt_namespace' => $page->getNamespace(), 'pt_title' => $page->getDBkey() ], |
528 | __METHOD__, |
529 | [], |
530 | $commentQuery['joins'] |
531 | ); |
532 | |
533 | if ( $row ) { |
534 | $cacheEntry['create_protection'] = [ |
535 | 'user' => $row->pt_user, |
536 | 'expiry' => $dbr->decodeExpiry( $row->pt_expiry ), |
537 | 'permission' => $row->pt_create_perm, |
538 | 'reason' => $this->commentStore->getComment( 'pt_reason', $row )->text, |
539 | ]; |
540 | } else { |
541 | $cacheEntry['create_protection'] = null; |
542 | } |
543 | |
544 | } |
545 | |
546 | return $cacheEntry['create_protection']; |
547 | } |
548 | |
549 | /** |
550 | * Cascading protection: Get the source of any cascading restrictions on this page. |
551 | * |
552 | * @param PageIdentity $page Must be local |
553 | * @return array[] Two elements: First is an array of PageIdentity objects of the pages from |
554 | * which cascading restrictions have come, which may be empty. Second is an array like that |
555 | * returned by getAllRestrictions(). |
556 | */ |
557 | public function getCascadeProtectionSources( PageIdentity $page ): array { |
558 | $page->assertWiki( PageIdentity::LOCAL ); |
559 | |
560 | return $this->getCascadeProtectionSourcesInternal( $page, false ); |
561 | } |
562 | |
563 | /** |
564 | * Cascading protection: Get the source of any cascading restrictions on this page. |
565 | * |
566 | * @param PageIdentity $page Must be local |
567 | * @param bool $shortCircuit If true, just return true or false instead of the actual lists. |
568 | * @return array|bool If $shortCircuit is true, return true if there is some cascading |
569 | * protection and false otherwise. Otherwise, same as getCascadeProtectionSources(). |
570 | */ |
571 | private function getCascadeProtectionSourcesInternal( |
572 | PageIdentity $page, bool $shortCircuit = false |
573 | ) { |
574 | if ( !$page->canExist() ) { |
575 | return $shortCircuit ? false : [ [], [] ]; |
576 | } |
577 | |
578 | $cacheEntry = &$this->cache[CacheKeyHelper::getKeyForPage( $page )]; |
579 | |
580 | if ( !$shortCircuit && isset( $cacheEntry['cascade_sources'] ) ) { |
581 | return $cacheEntry['cascade_sources']; |
582 | } elseif ( $shortCircuit && isset( $cacheEntry['has_cascading'] ) ) { |
583 | return $cacheEntry['has_cascading']; |
584 | } |
585 | |
586 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
587 | $queryBuilder = $dbr->newSelectQueryBuilder(); |
588 | $queryBuilder->select( [ 'pr_expiry' ] ) |
589 | ->from( 'page_restrictions' ) |
590 | ->where( [ 'pr_cascade' => 1 ] ); |
591 | |
592 | if ( $page->getNamespace() === NS_FILE ) { |
593 | // Files transclusion may receive cascading protection in the future |
594 | // see https://phabricator.wikimedia.org/T241453 |
595 | $queryBuilder->join( 'imagelinks', null, 'il_from=pr_page' ); |
596 | $queryBuilder->andWhere( [ 'il_to' => $page->getDBkey() ] ); |
597 | } else { |
598 | $queryBuilder->join( 'templatelinks', null, 'tl_from=pr_page' ); |
599 | $queryBuilder->andWhere( |
600 | $this->linksMigration->getLinksConditions( |
601 | 'templatelinks', |
602 | TitleValue::newFromPage( $page ) |
603 | ) |
604 | ); |
605 | } |
606 | |
607 | if ( !$shortCircuit ) { |
608 | $queryBuilder->fields( [ 'pr_page', 'page_namespace', 'page_title', 'pr_type', 'pr_level' ] ); |
609 | $queryBuilder->join( 'page', null, 'page_id=pr_page' ); |
610 | } |
611 | |
612 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
613 | |
614 | $sources = []; |
615 | $pageRestrictions = []; |
616 | $now = wfTimestampNow(); |
617 | |
618 | foreach ( $res as $row ) { |
619 | $expiry = $dbr->decodeExpiry( $row->pr_expiry ); |
620 | if ( $expiry > $now ) { |
621 | if ( $shortCircuit ) { |
622 | $cacheEntry['has_cascading'] = true; |
623 | return true; |
624 | } |
625 | |
626 | $sources[$row->pr_page] = new PageIdentityValue( $row->pr_page, |
627 | $row->page_namespace, $row->page_title, PageIdentity::LOCAL ); |
628 | // Add groups needed for each restriction type if its not already there |
629 | // Make sure this restriction type still exists |
630 | |
631 | if ( !isset( $pageRestrictions[$row->pr_type] ) ) { |
632 | $pageRestrictions[$row->pr_type] = []; |
633 | } |
634 | |
635 | if ( !in_array( $row->pr_level, $pageRestrictions[$row->pr_type] ) ) { |
636 | $pageRestrictions[$row->pr_type][] = $row->pr_level; |
637 | } |
638 | } |
639 | } |
640 | |
641 | $cacheEntry['has_cascading'] = (bool)$sources; |
642 | |
643 | if ( $shortCircuit ) { |
644 | return false; |
645 | } |
646 | |
647 | $cacheEntry['cascade_sources'] = [ $sources, $pageRestrictions ]; |
648 | return [ $sources, $pageRestrictions ]; |
649 | } |
650 | |
651 | /** |
652 | * @param PageIdentity $page Must be local |
653 | * @return bool Whether or not the page's restrictions have already been loaded from the |
654 | * database |
655 | */ |
656 | public function areRestrictionsLoaded( PageIdentity $page ): bool { |
657 | $page->assertWiki( PageIdentity::LOCAL ); |
658 | |
659 | return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['restrictions'] ); |
660 | } |
661 | |
662 | /** |
663 | * Determines whether cascading protection sources have already been loaded from the database. |
664 | * |
665 | * @param PageIdentity $page Must be local |
666 | * @return bool |
667 | */ |
668 | public function areCascadeProtectionSourcesLoaded( PageIdentity $page ): bool { |
669 | $page->assertWiki( PageIdentity::LOCAL ); |
670 | |
671 | return isset( $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade_sources'] ); |
672 | } |
673 | |
674 | /** |
675 | * Checks if restrictions are cascading for the current page |
676 | * |
677 | * @param PageIdentity $page Must be local |
678 | * @return bool |
679 | */ |
680 | public function areRestrictionsCascading( PageIdentity $page ): bool { |
681 | $page->assertWiki( PageIdentity::LOCAL ); |
682 | |
683 | if ( !$this->areRestrictionsLoaded( $page ) ) { |
684 | $this->loadRestrictions( $page ); |
685 | } |
686 | return $this->cache[CacheKeyHelper::getKeyForPage( $page )]['cascade'] ?? false; |
687 | } |
688 | |
689 | /** |
690 | * Flush the protection cache in this object and force reload from the database. This is used |
691 | * when updating protection from WikiPage::doUpdateRestrictions(). |
692 | * |
693 | * @param PageIdentity $page Must be local |
694 | * @internal |
695 | */ |
696 | public function flushRestrictions( PageIdentity $page ): void { |
697 | $page->assertWiki( PageIdentity::LOCAL ); |
698 | |
699 | unset( $this->cache[CacheKeyHelper::getKeyForPage( $page )] ); |
700 | } |
701 | |
702 | } |