Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.73% |
408 / 440 |
|
44.12% |
15 / 34 |
CRAP | |
0.00% |
0 / 1 |
ContributionsPager | |
92.73% |
408 / 440 |
|
44.12% |
15 / 34 |
128.82 | |
0.00% |
0 / 1 |
__construct | |
95.83% |
46 / 48 |
|
0.00% |
0 / 1 |
8 | |||
getDefaultQuery | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
reallyDoQuery | |
97.06% |
33 / 34 |
|
0.00% |
0 / 1 |
8 | |||
getRevisionQuery | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getQueryInfo | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
10 | |||
getNamespaceCond | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
getTagFilter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTagInvert | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTarget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isNewOnly | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNamespace | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doBatchLookups | |
76.92% |
30 / 39 |
|
0.00% |
0 / 1 |
11.23 | |||
getStartBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEndBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tryCreatingRevisionRecord | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
8 | |||
createRevisionRecord | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
populateAttributes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatArticleLink | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
formatDiffHistLinks | |
98.33% |
59 / 60 |
|
0.00% |
0 / 1 |
7 | |||
formatDateLink | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
7.01 | |||
formatTopMarkText | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
7 | |||
formatCharDiff | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
3 | |||
formatComment | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
formatUserLink | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
5.01 | |||
formatFlags | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
formatVisibilityLink | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
formatTags | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
revisionUserIsDeleted | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
92.00% |
23 / 25 |
|
0.00% |
0 / 1 |
10.05 | |||
getTemplateParams | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
3 | |||
getProcessedTemplate | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getSqlComment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
preventClickjacking | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPreventClickjacking | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPreventClickjacking | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
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 | * @ingroup Pager |
20 | */ |
21 | |
22 | namespace MediaWiki\Pager; |
23 | |
24 | use ChangesList; |
25 | use ChangeTags; |
26 | use HtmlArmor; |
27 | use InvalidArgumentException; |
28 | use MapCacheLRU; |
29 | use MediaWiki\Cache\LinkBatchFactory; |
30 | use MediaWiki\CommentFormatter\CommentFormatter; |
31 | use MediaWiki\Context\IContextSource; |
32 | use MediaWiki\HookContainer\HookContainer; |
33 | use MediaWiki\HookContainer\HookRunner; |
34 | use MediaWiki\Html\Html; |
35 | use MediaWiki\Html\TemplateParser; |
36 | use MediaWiki\Linker\Linker; |
37 | use MediaWiki\Linker\LinkRenderer; |
38 | use MediaWiki\MainConfigNames; |
39 | use MediaWiki\MediaWikiServices; |
40 | use MediaWiki\Parser\Sanitizer; |
41 | use MediaWiki\Revision\RevisionRecord; |
42 | use MediaWiki\Revision\RevisionStore; |
43 | use MediaWiki\SpecialPage\SpecialPage; |
44 | use MediaWiki\Title\NamespaceInfo; |
45 | use MediaWiki\Title\Title; |
46 | use MediaWiki\User\UserFactory; |
47 | use MediaWiki\User\UserIdentity; |
48 | use MediaWiki\User\UserRigorOptions; |
49 | use stdClass; |
50 | use Wikimedia\Rdbms\FakeResultWrapper; |
51 | use Wikimedia\Rdbms\IResultWrapper; |
52 | |
53 | /** |
54 | * Pager for Special:Contributions |
55 | * @ingroup Pager |
56 | */ |
57 | abstract class ContributionsPager extends RangeChronologicalPager { |
58 | |
59 | /** @inheritDoc */ |
60 | public $mGroupByDate = true; |
61 | |
62 | /** |
63 | * @var string[] Local cache for escaped messages |
64 | */ |
65 | protected $messages; |
66 | |
67 | /** |
68 | * @var bool Get revisions from the archive table (if true) or the revision table (if false) |
69 | */ |
70 | protected $isArchive; |
71 | |
72 | /** |
73 | * @var string User name, or a string describing an IP address range |
74 | */ |
75 | protected $target; |
76 | |
77 | /** |
78 | * @var string|int A single namespace number, or an empty string for all namespaces |
79 | */ |
80 | private $namespace; |
81 | |
82 | /** |
83 | * @var string[]|false Name of tag to filter, or false to ignore tags |
84 | */ |
85 | private $tagFilter; |
86 | |
87 | /** |
88 | * @var bool Set to true to invert the tag selection |
89 | */ |
90 | private $tagInvert; |
91 | |
92 | /** |
93 | * @var bool Set to true to invert the namespace selection |
94 | */ |
95 | private $nsInvert; |
96 | |
97 | /** |
98 | * @var bool Set to true to show both the subject and talk namespace, no matter which got |
99 | * selected |
100 | */ |
101 | private $associated; |
102 | |
103 | /** |
104 | * @var bool Set to true to show only deleted revisions |
105 | */ |
106 | private $deletedOnly; |
107 | |
108 | /** |
109 | * @var bool Set to true to show only latest (a.k.a. current) revisions |
110 | */ |
111 | private $topOnly; |
112 | |
113 | /** |
114 | * @var bool Set to true to show only new pages |
115 | */ |
116 | private $newOnly; |
117 | |
118 | /** |
119 | * @var bool Set to true to hide edits marked as minor by the user |
120 | */ |
121 | private $hideMinor; |
122 | |
123 | /** |
124 | * @var bool Set to true to only include mediawiki revisions. |
125 | * (restricts extensions from executing additional queries to include their own contributions) |
126 | */ |
127 | private $revisionsOnly; |
128 | |
129 | /** @var bool */ |
130 | private $preventClickjacking = false; |
131 | |
132 | protected ?Title $currentPage; |
133 | protected ?RevisionRecord $currentRevRecord; |
134 | |
135 | /** |
136 | * @var array |
137 | */ |
138 | private $mParentLens; |
139 | |
140 | /** @var UserIdentity */ |
141 | protected $targetUser; |
142 | |
143 | /** |
144 | * Set to protected to allow subclasses access for overrides |
145 | */ |
146 | protected TemplateParser $templateParser; |
147 | |
148 | private CommentFormatter $commentFormatter; |
149 | private HookRunner $hookRunner; |
150 | private LinkBatchFactory $linkBatchFactory; |
151 | protected NamespaceInfo $namespaceInfo; |
152 | protected RevisionStore $revisionStore; |
153 | |
154 | /** @var string[] */ |
155 | private $formattedComments = []; |
156 | |
157 | /** @var RevisionRecord[] Cached revisions by ID */ |
158 | private $revisions = []; |
159 | |
160 | /** @var MapCacheLRU */ |
161 | private $tagsCache; |
162 | |
163 | /** |
164 | * Field names for various attributes. These may be overridden in a subclass, |
165 | * for example for getting revisions from the archive table. |
166 | */ |
167 | protected string $revisionIdField = 'rev_id'; |
168 | protected string $revisionParentIdField = 'rev_parent_id'; |
169 | protected string $revisionTimestampField = 'rev_timestamp'; |
170 | protected string $revisionLengthField = 'rev_len'; |
171 | protected string $revisionDeletedField = 'rev_deleted'; |
172 | protected string $revisionMinorField = 'rev_minor_edit'; |
173 | protected string $userNameField = 'rev_user_text'; |
174 | protected string $pageNamespaceField = 'page_namespace'; |
175 | protected string $pageTitleField = 'page_title'; |
176 | |
177 | /** |
178 | * @param LinkRenderer $linkRenderer |
179 | * @param LinkBatchFactory $linkBatchFactory |
180 | * @param HookContainer $hookContainer |
181 | * @param RevisionStore $revisionStore |
182 | * @param NamespaceInfo $namespaceInfo |
183 | * @param CommentFormatter $commentFormatter |
184 | * @param UserFactory $userFactory |
185 | * @param IContextSource $context |
186 | * @param array $options |
187 | * @param UserIdentity|null $targetUser |
188 | */ |
189 | public function __construct( |
190 | LinkRenderer $linkRenderer, |
191 | LinkBatchFactory $linkBatchFactory, |
192 | HookContainer $hookContainer, |
193 | RevisionStore $revisionStore, |
194 | NamespaceInfo $namespaceInfo, |
195 | CommentFormatter $commentFormatter, |
196 | UserFactory $userFactory, |
197 | IContextSource $context, |
198 | array $options, |
199 | ?UserIdentity $targetUser |
200 | ) { |
201 | $this->isArchive = $options['isArchive'] ?? false; |
202 | |
203 | // Set ->target before calling parent::__construct() so |
204 | // parent can call $this->getIndexField() and get the right result. Set |
205 | // the rest too just to keep things simple. |
206 | if ( $targetUser ) { |
207 | $this->target = $options['target'] ?? $targetUser->getName(); |
208 | $this->targetUser = $targetUser; |
209 | } else { |
210 | // Use target option |
211 | // It's possible for the target to be empty. This is used by |
212 | // ContribsPagerTest and does not cause newFromName() to return |
213 | // false. It's probably not used by any production code. |
214 | $this->target = $options['target'] ?? ''; |
215 | // @phan-suppress-next-line PhanPossiblyNullTypeMismatchProperty RIGOR_NONE never returns null |
216 | $this->targetUser = $userFactory->newFromName( |
217 | $this->target, UserRigorOptions::RIGOR_NONE |
218 | ); |
219 | if ( !$this->targetUser ) { |
220 | // This can happen if the target contained "#". Callers |
221 | // typically pass user input through title normalization to |
222 | // avoid it. |
223 | throw new InvalidArgumentException( __METHOD__ . ': the user name is too ' . |
224 | 'broken to use even with validation disabled.' ); |
225 | } |
226 | } |
227 | |
228 | $this->namespace = $options['namespace'] ?? ''; |
229 | $this->tagFilter = $options['tagfilter'] ?? false; |
230 | $this->tagInvert = $options['tagInvert'] ?? false; |
231 | $this->nsInvert = $options['nsInvert'] ?? false; |
232 | $this->associated = $options['associated'] ?? false; |
233 | |
234 | $this->deletedOnly = !empty( $options['deletedOnly'] ); |
235 | $this->topOnly = !empty( $options['topOnly'] ); |
236 | $this->newOnly = !empty( $options['newOnly'] ); |
237 | $this->hideMinor = !empty( $options['hideMinor'] ); |
238 | $this->revisionsOnly = !empty( $options['revisionsOnly'] ); |
239 | |
240 | parent::__construct( $context, $linkRenderer ); |
241 | |
242 | $msgs = [ |
243 | 'diff', |
244 | 'hist', |
245 | 'pipe-separator', |
246 | 'uctop', |
247 | 'changeslist-nocomment', |
248 | 'undeleteviewlink', |
249 | 'undeleteviewlink', |
250 | 'deletionlog', |
251 | ]; |
252 | |
253 | foreach ( $msgs as $msg ) { |
254 | $this->messages[$msg] = $this->msg( $msg )->escaped(); |
255 | } |
256 | |
257 | // Date filtering: use timestamp if available |
258 | $startTimestamp = ''; |
259 | $endTimestamp = ''; |
260 | if ( isset( $options['start'] ) && $options['start'] ) { |
261 | $startTimestamp = $options['start'] . ' 00:00:00'; |
262 | } |
263 | if ( isset( $options['end'] ) && $options['end'] ) { |
264 | $endTimestamp = $options['end'] . ' 23:59:59'; |
265 | } |
266 | $this->getDateRangeCond( $startTimestamp, $endTimestamp ); |
267 | |
268 | $this->templateParser = new TemplateParser(); |
269 | $this->linkBatchFactory = $linkBatchFactory; |
270 | $this->hookRunner = new HookRunner( $hookContainer ); |
271 | $this->revisionStore = $revisionStore; |
272 | $this->namespaceInfo = $namespaceInfo; |
273 | $this->commentFormatter = $commentFormatter; |
274 | $this->tagsCache = new MapCacheLRU( 50 ); |
275 | } |
276 | |
277 | public function getDefaultQuery() { |
278 | $query = parent::getDefaultQuery(); |
279 | $query['target'] = $this->target; |
280 | |
281 | return $query; |
282 | } |
283 | |
284 | /** |
285 | * This method basically executes the exact same code as the parent class, though with |
286 | * a hook added, to allow extensions to add additional queries. |
287 | * |
288 | * @param string $offset Index offset, inclusive |
289 | * @param int $limit Exact query limit |
290 | * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING |
291 | * @return IResultWrapper |
292 | */ |
293 | public function reallyDoQuery( $offset, $limit, $order ) { |
294 | [ $tables, $fields, $conds, $fname, $options, $join_conds ] = $this->buildQueryInfo( |
295 | $offset, |
296 | $limit, |
297 | $order |
298 | ); |
299 | |
300 | $options['MAX_EXECUTION_TIME'] = |
301 | $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ); |
302 | /* |
303 | * This hook will allow extensions to add in additional queries, so they can get their data |
304 | * in My Contributions as well. Extensions should append their results to the $data array. |
305 | * |
306 | * Extension queries have to implement the navbar requirement as well. They should |
307 | * - have a column aliased as $pager->getIndexField() |
308 | * - have LIMIT set |
309 | * - have a WHERE-clause that compares the $pager->getIndexField()-equivalent column to the offset |
310 | * - have the ORDER BY specified based upon the details provided by the navbar |
311 | * |
312 | * See includes/Pager.php buildQueryInfo() method on how to build LIMIT, WHERE & ORDER BY |
313 | * |
314 | * &$data: an array of results of all contribs queries |
315 | * $pager: the ContribsPager object hooked into |
316 | * $offset: see phpdoc above |
317 | * $limit: see phpdoc above |
318 | * $descending: see phpdoc above |
319 | */ |
320 | $dbr = $this->getDatabase(); |
321 | $data = [ $dbr->newSelectQueryBuilder() |
322 | ->tables( is_array( $tables ) ? $tables : [ $tables ] ) |
323 | ->fields( $fields ) |
324 | ->conds( $conds ) |
325 | ->caller( $fname ) |
326 | ->options( $options ) |
327 | ->joinConds( $join_conds ) |
328 | ->setMaxExecutionTime( $this->getConfig()->get( MainConfigNames::MaxExecutionTimeForExpensiveQueries ) ) |
329 | ->fetchResultSet() ]; |
330 | if ( !$this->revisionsOnly ) { |
331 | // These hooks were moved from ContribsPager and DeletedContribsPager. For backwards |
332 | // compatability, they keep the same names. But they should be run for any contributions |
333 | // pager, otherwise the entries from extensions would be missing. |
334 | $reallyDoQueryHook = $this->isArchive ? |
335 | 'onDeletedContribsPager__reallyDoQuery' : |
336 | 'onContribsPager__reallyDoQuery'; |
337 | // TODO: Range offsets are fairly important and all handlers should take care of it. |
338 | // If this hook will be replaced (e.g. unified with the DeletedContribsPager one), |
339 | // please consider passing [ $this->endOffset, $this->startOffset ] to it (T167577). |
340 | $this->hookRunner->$reallyDoQueryHook( $data, $this, $offset, $limit, $order ); |
341 | } |
342 | |
343 | $result = []; |
344 | |
345 | // loop all results and collect them in an array |
346 | foreach ( $data as $query ) { |
347 | foreach ( $query as $i => $row ) { |
348 | // If the query results are in descending order, the indexes must also be in descending order |
349 | $index = $order === self::QUERY_ASCENDING ? $i : $limit - 1 - $i; |
350 | // Left-pad with zeroes, because these values will be sorted as strings |
351 | $index = str_pad( (string)$index, strlen( (string)$limit ), '0', STR_PAD_LEFT ); |
352 | // use index column as key, allowing us to easily sort in PHP |
353 | $result[$row->{$this->getIndexField()} . "-$index"] = $row; |
354 | } |
355 | } |
356 | |
357 | // sort results |
358 | if ( $order === self::QUERY_ASCENDING ) { |
359 | ksort( $result ); |
360 | } else { |
361 | krsort( $result ); |
362 | } |
363 | |
364 | // enforce limit |
365 | $result = array_slice( $result, 0, $limit ); |
366 | |
367 | // get rid of array keys |
368 | $result = array_values( $result ); |
369 | |
370 | return new FakeResultWrapper( $result ); |
371 | } |
372 | |
373 | /** |
374 | * Get queryInfo for the main query selecting revisions, not including |
375 | * filtering on namespace, date, etc. |
376 | * |
377 | * @return array |
378 | */ |
379 | abstract protected function getRevisionQuery(); |
380 | |
381 | public function getQueryInfo() { |
382 | $queryInfo = $this->getRevisionQuery(); |
383 | |
384 | if ( $this->deletedOnly ) { |
385 | $queryInfo['conds'][] = $this->revisionDeletedField . ' != 0'; |
386 | } |
387 | |
388 | if ( !$this->isArchive && $this->topOnly ) { |
389 | $queryInfo['conds'][] = $this->revisionIdField . ' = page_latest'; |
390 | } |
391 | |
392 | if ( $this->newOnly ) { |
393 | $queryInfo['conds'][] = $this->revisionParentIdField . ' = 0'; |
394 | } |
395 | |
396 | if ( $this->hideMinor ) { |
397 | $queryInfo['conds'][] = $this->revisionMinorField . ' = 0'; |
398 | } |
399 | |
400 | $queryInfo['conds'] = array_merge( $queryInfo['conds'], $this->getNamespaceCond() ); |
401 | |
402 | // Paranoia: avoid brute force searches (T19342) |
403 | $dbr = $this->getDatabase(); |
404 | if ( !$this->getAuthority()->isAllowed( 'deletedhistory' ) ) { |
405 | $queryInfo['conds'][] = $dbr->bitAnd( |
406 | $this->revisionDeletedField, RevisionRecord::DELETED_USER |
407 | ) . ' = 0'; |
408 | } elseif ( !$this->getAuthority()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { |
409 | $queryInfo['conds'][] = $dbr->bitAnd( |
410 | $this->revisionDeletedField, RevisionRecord::SUPPRESSED_USER |
411 | ) . ' != ' . RevisionRecord::SUPPRESSED_USER; |
412 | } |
413 | |
414 | // $this->getIndexField() must be in the result rows, as reallyDoQuery() tries to access it. |
415 | $indexField = $this->getIndexField(); |
416 | if ( $indexField !== $this->revisionTimestampField ) { |
417 | $queryInfo['fields'][] = $indexField; |
418 | } |
419 | |
420 | MediaWikiServices::getInstance()->getChangeTagsStore()->modifyDisplayQuery( |
421 | $queryInfo['tables'], |
422 | $queryInfo['fields'], |
423 | $queryInfo['conds'], |
424 | $queryInfo['join_conds'], |
425 | $queryInfo['options'], |
426 | $this->tagFilter, |
427 | $this->tagInvert, |
428 | ); |
429 | |
430 | if ( !$this->isArchive ) { |
431 | $this->hookRunner->onContribsPager__getQueryInfo( $this, $queryInfo ); |
432 | } |
433 | |
434 | return $queryInfo; |
435 | } |
436 | |
437 | protected function getNamespaceCond() { |
438 | if ( $this->namespace !== '' ) { |
439 | $dbr = $this->getDatabase(); |
440 | $namespaces = [ $this->namespace ]; |
441 | $eq_op = $this->nsInvert ? '!=' : '='; |
442 | if ( $this->associated ) { |
443 | $namespaces[] = $this->namespaceInfo->getAssociated( $this->namespace ); |
444 | } |
445 | return [ $dbr->expr( $this->pageNamespaceField, $eq_op, $namespaces ) ]; |
446 | } |
447 | |
448 | return []; |
449 | } |
450 | |
451 | /** |
452 | * @return false|string[] |
453 | */ |
454 | public function getTagFilter() { |
455 | return $this->tagFilter; |
456 | } |
457 | |
458 | /** |
459 | * @return bool |
460 | */ |
461 | public function getTagInvert() { |
462 | return $this->tagInvert; |
463 | } |
464 | |
465 | /** |
466 | * @return string |
467 | */ |
468 | public function getTarget() { |
469 | return $this->target; |
470 | } |
471 | |
472 | /** |
473 | * @return bool |
474 | */ |
475 | public function isNewOnly() { |
476 | return $this->newOnly; |
477 | } |
478 | |
479 | /** |
480 | * @return int|string |
481 | */ |
482 | public function getNamespace() { |
483 | return $this->namespace; |
484 | } |
485 | |
486 | protected function doBatchLookups() { |
487 | # Do a link batch query |
488 | $this->mResult->seek( 0 ); |
489 | $parentRevIds = []; |
490 | $this->mParentLens = []; |
491 | $revisions = []; |
492 | $linkBatch = $this->linkBatchFactory->newLinkBatch(); |
493 | # Give some pointers to make (last) links |
494 | foreach ( $this->mResult as $row ) { |
495 | $revisionRecord = $this->tryCreatingRevisionRecord( $row ); |
496 | if ( !$revisionRecord ) { |
497 | continue; |
498 | } |
499 | if ( isset( $row->{$this->revisionParentIdField} ) && $row->{$this->revisionParentIdField} ) { |
500 | $parentRevIds[] = (int)$row->{$this->revisionParentIdField}; |
501 | } |
502 | $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField}; |
503 | if ( $this->target !== $row->{$this->userNameField} ) { |
504 | // If the target does not match the author, batch the author's talk page |
505 | $linkBatch->add( NS_USER_TALK, $row->{$this->userNameField} ); |
506 | } |
507 | $linkBatch->add( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} ); |
508 | $revisions[$row->{$this->revisionIdField}] = $this->createRevisionRecord( $row ); |
509 | } |
510 | // Fetch rev_len/ar_len for revisions not already scanned above |
511 | // TODO: is it possible to make this fully abstract? |
512 | if ( $this->isArchive ) { |
513 | $parentRevIds = array_diff( $parentRevIds, array_keys( $this->mParentLens ) ); |
514 | if ( $parentRevIds ) { |
515 | $result = $this->revisionStore |
516 | ->newArchiveSelectQueryBuilder( $this->getDatabase() ) |
517 | ->clearFields() |
518 | ->fields( [ $this->revisionIdField, $this->revisionLengthField ] ) |
519 | ->where( [ $this->revisionIdField => $parentRevIds ] ) |
520 | ->caller( __METHOD__ ) |
521 | ->fetchResultSet(); |
522 | foreach ( $result as $row ) { |
523 | $this->mParentLens[(int)$row->{$this->revisionIdField}] = $row->{$this->revisionLengthField}; |
524 | } |
525 | } |
526 | } |
527 | $this->mParentLens += $this->revisionStore->getRevisionSizes( |
528 | array_diff( $parentRevIds, array_keys( $this->mParentLens ) ) |
529 | ); |
530 | $linkBatch->execute(); |
531 | |
532 | $revisionBatch = $this->commentFormatter->createRevisionBatch() |
533 | ->authority( $this->getAuthority() ) |
534 | ->revisions( $revisions ); |
535 | |
536 | if ( !$this->isArchive ) { |
537 | // Only show public comments, because this page might be public |
538 | $revisionBatch = $revisionBatch->hideIfDeleted(); |
539 | } |
540 | |
541 | $this->formattedComments = $revisionBatch->execute(); |
542 | |
543 | # For performance, save the revision objects for later. |
544 | # The array is indexed by rev_id. doBatchLookups() may be called |
545 | # multiple times with different results, so merge the revisions array, |
546 | # ignoring any duplicates. |
547 | $this->revisions += $revisions; |
548 | } |
549 | |
550 | /** |
551 | * @inheritDoc |
552 | */ |
553 | protected function getStartBody() { |
554 | return "<section class='mw-pager-body'>\n"; |
555 | } |
556 | |
557 | /** |
558 | * @inheritDoc |
559 | */ |
560 | protected function getEndBody() { |
561 | return "</section>\n"; |
562 | } |
563 | |
564 | /** |
565 | * If the object looks like a revision row, or corresponds to a previously |
566 | * cached revision, return the RevisionRecord. Otherwise, return null. |
567 | * |
568 | * @since 1.35 |
569 | * |
570 | * @param mixed $row |
571 | * @param Title|null $title |
572 | * @return RevisionRecord|null |
573 | */ |
574 | public function tryCreatingRevisionRecord( $row, $title = null ) { |
575 | if ( $row instanceof stdClass && isset( $row->{$this->revisionIdField} ) |
576 | && isset( $this->revisions[$row->{$this->revisionIdField}] ) |
577 | ) { |
578 | return $this->revisions[$row->{$this->revisionIdField}]; |
579 | } |
580 | |
581 | if ( |
582 | $this->isArchive && |
583 | $this->revisionStore->isRevisionRow( $row, 'archive' ) |
584 | ) { |
585 | return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title ); |
586 | } |
587 | |
588 | if ( |
589 | !$this->isArchive && |
590 | $this->revisionStore->isRevisionRow( $row ) |
591 | ) { |
592 | return $this->revisionStore->newRevisionFromRow( $row, 0, $title ); |
593 | } |
594 | |
595 | return null; |
596 | } |
597 | |
598 | /** |
599 | * Create a revision record from a $row that models a revision. |
600 | * |
601 | * @param mixed $row |
602 | * @param Title|null $title |
603 | * @return RevisionRecord |
604 | */ |
605 | public function createRevisionRecord( $row, $title = null ) { |
606 | if ( $this->isArchive ) { |
607 | return $this->revisionStore->newRevisionFromArchiveRow( $row, 0, $title ); |
608 | } |
609 | |
610 | return $this->revisionStore->newRevisionFromRow( $row, 0, $title ); |
611 | } |
612 | |
613 | /** |
614 | * Populate the HTML attributes. |
615 | * |
616 | * @param mixed $row |
617 | * @param string[] &$attributes |
618 | */ |
619 | protected function populateAttributes( $row, &$attributes ) { |
620 | $attributes['data-mw-revid'] = $this->currentRevRecord->getId(); |
621 | } |
622 | |
623 | /** |
624 | * Format a link to an article. |
625 | * |
626 | * @param mixed $row |
627 | * @return string |
628 | */ |
629 | protected function formatArticleLink( $row ) { |
630 | if ( !$this->currentPage ) { |
631 | return ''; |
632 | } |
633 | $dir = $this->getLanguage()->getDir(); |
634 | return Html::rawElement( 'bdi', [ 'dir' => $dir ], $this->getLinkRenderer()->makeLink( |
635 | $this->currentPage, |
636 | $this->currentPage->getPrefixedText(), |
637 | [ 'class' => 'mw-contributions-title' ], |
638 | $this->currentPage->isRedirect() ? [ 'redirect' => 'no' ] : [] |
639 | ) ); |
640 | } |
641 | |
642 | /** |
643 | * Format diff and history links. |
644 | * |
645 | * @param mixed $row |
646 | * @return string |
647 | */ |
648 | protected function formatDiffHistLinks( $row ) { |
649 | if ( !$this->currentPage || !$this->currentRevRecord ) { |
650 | return ''; |
651 | } |
652 | if ( $this->isArchive ) { |
653 | // Add the same links as DeletedContribsPager::formatRevisionRow |
654 | $undelete = SpecialPage::getTitleFor( 'Undelete' ); |
655 | if ( $this->getAuthority()->isAllowed( 'deletedtext' ) ) { |
656 | $last = $this->getLinkRenderer()->makeKnownLink( |
657 | $undelete, |
658 | new HtmlArmor( $this->messages['diff'] ), |
659 | [], |
660 | [ |
661 | 'target' => $this->currentPage->getPrefixedText(), |
662 | 'timestamp' => $this->currentRevRecord->getTimestamp(), |
663 | 'diff' => 'prev' |
664 | ] |
665 | ); |
666 | } else { |
667 | $last = $this->messages['diff']; |
668 | } |
669 | |
670 | $logs = SpecialPage::getTitleFor( 'Log' ); |
671 | $dellog = $this->getLinkRenderer()->makeKnownLink( |
672 | $logs, |
673 | new HtmlArmor( $this->messages['deletionlog'] ), |
674 | [], |
675 | [ |
676 | 'type' => 'delete', |
677 | 'page' => $this->currentPage->getPrefixedText() |
678 | ] |
679 | ); |
680 | |
681 | $reviewlink = $this->getLinkRenderer()->makeKnownLink( |
682 | SpecialPage::getTitleFor( 'Undelete', $this->currentPage->getPrefixedDBkey() ), |
683 | new HtmlArmor( $this->messages['undeleteviewlink'] ) |
684 | ); |
685 | |
686 | return Html::rawElement( |
687 | 'span', |
688 | [ 'class' => 'mw-deletedcontribs-tools' ], |
689 | $this->msg( 'parentheses' )->rawParams( $this->getLanguage()->pipeList( |
690 | [ $last, $dellog, $reviewlink ] ) )->escaped() |
691 | ); |
692 | } else { |
693 | # Is there a visible previous revision? |
694 | if ( $this->currentRevRecord->getParentId() !== 0 && |
695 | $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) |
696 | ) { |
697 | $difftext = $this->getLinkRenderer()->makeKnownLink( |
698 | $this->currentPage, |
699 | new HtmlArmor( $this->messages['diff'] ), |
700 | [ 'class' => 'mw-changeslist-diff' ], |
701 | [ |
702 | 'diff' => 'prev', |
703 | 'oldid' => $row->{$this->revisionIdField}, |
704 | ] |
705 | ); |
706 | } else { |
707 | $difftext = $this->messages['diff']; |
708 | } |
709 | $histlink = $this->getLinkRenderer()->makeKnownLink( |
710 | $this->currentPage, |
711 | new HtmlArmor( $this->messages['hist'] ), |
712 | [ 'class' => 'mw-changeslist-history' ], |
713 | [ 'action' => 'history' ] |
714 | ); |
715 | |
716 | // While it might be tempting to use a list here |
717 | // this would result in clutter and slows down navigating the content |
718 | // in assistive technology. |
719 | // See https://phabricator.wikimedia.org/T205581#4734812 |
720 | return Html::rawElement( 'span', |
721 | [ 'class' => 'mw-changeslist-links' ], |
722 | // The spans are needed to ensure the dividing '|' elements are not |
723 | // themselves styled as links. |
724 | Html::rawElement( 'span', [], $difftext ) . |
725 | ' ' . // Space needed for separating two words. |
726 | Html::rawElement( 'span', [], $histlink ) |
727 | ); |
728 | } |
729 | } |
730 | |
731 | /** |
732 | * Format a date link. |
733 | * |
734 | * @param mixed $row |
735 | * @return string |
736 | */ |
737 | protected function formatDateLink( $row ) { |
738 | if ( !$this->currentPage || !$this->currentRevRecord ) { |
739 | return ''; |
740 | } |
741 | if ( $this->isArchive ) { |
742 | $date = $this->getLanguage()->userTimeAndDate( |
743 | $this->currentRevRecord->getTimestamp(), |
744 | $this->getUser() |
745 | ); |
746 | |
747 | if ( $this->getAuthority()->isAllowed( 'undelete' ) && |
748 | $this->currentRevRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) |
749 | ) { |
750 | $dateLink = $this->getLinkRenderer()->makeKnownLink( |
751 | SpecialPage::getTitleFor( 'Undelete' ), |
752 | $date, |
753 | [ 'class' => 'mw-changeslist-date' ], |
754 | [ |
755 | 'target' => $this->currentPage->getPrefixedText(), |
756 | 'timestamp' => $this->currentRevRecord->getTimestamp() |
757 | ] |
758 | ); |
759 | } else { |
760 | $dateLink = htmlspecialchars( $date ); |
761 | } |
762 | if ( $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) { |
763 | $class = Linker::getRevisionDeletedClass( $this->currentRevRecord ); |
764 | $dateLink = Html::rawElement( |
765 | 'span', |
766 | [ 'class' => $class ], |
767 | $dateLink |
768 | ); |
769 | } |
770 | } else { |
771 | $dateLink = ChangesList::revDateLink( |
772 | $this->currentRevRecord, |
773 | $this->getAuthority(), |
774 | $this->getLanguage(), |
775 | $this->currentPage |
776 | ); |
777 | } |
778 | return $dateLink; |
779 | } |
780 | |
781 | /** |
782 | * Format annotation and add extra class if a row represents a latest revision. |
783 | * |
784 | * @param mixed $row |
785 | * @param string[] &$classes |
786 | * @return string |
787 | */ |
788 | protected function formatTopMarkText( $row, &$classes ) { |
789 | if ( !$this->currentPage || !$this->currentRevRecord ) { |
790 | return ''; |
791 | } |
792 | $topmarktext = ''; |
793 | if ( !$this->isArchive ) { |
794 | $pagerTools = new PagerTools( |
795 | $this->currentRevRecord, |
796 | null, |
797 | $row->{$this->revisionIdField} === $row->page_latest && !$row->page_is_new, |
798 | $this->hookRunner, |
799 | $this->currentPage, |
800 | $this->getContext(), |
801 | $this->getLinkRenderer() |
802 | ); |
803 | if ( $row->{$this->revisionIdField} === $row->page_latest ) { |
804 | $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>'; |
805 | $classes[] = 'mw-contributions-current'; |
806 | } |
807 | if ( $pagerTools->shouldPreventClickjacking() ) { |
808 | $this->setPreventClickjacking( true ); |
809 | } |
810 | $topmarktext .= $pagerTools->toHTML(); |
811 | } |
812 | return $topmarktext; |
813 | } |
814 | |
815 | /** |
816 | * Format annotation to show the size of a diff. |
817 | * |
818 | * @param mixed $row |
819 | * @return string |
820 | */ |
821 | protected function formatCharDiff( $row ) { |
822 | if ( $row->{$this->revisionParentIdField} === null ) { |
823 | // For some reason rev_parent_id isn't populated for this row. |
824 | // Its rumoured this is true on wikipedia for some revisions (T36922). |
825 | // Next best thing is to have the total number of bytes. |
826 | $chardiff = ' <span class="mw-changeslist-separator"></span> '; |
827 | $chardiff .= Linker::formatRevisionSize( $row->{$this->revisionLengthField} ); |
828 | $chardiff .= ' <span class="mw-changeslist-separator"></span> '; |
829 | } else { |
830 | $parentLen = 0; |
831 | if ( isset( $this->mParentLens[$row->{$this->revisionParentIdField}] ) ) { |
832 | $parentLen = $this->mParentLens[$row->{$this->revisionParentIdField}]; |
833 | } |
834 | |
835 | $chardiff = ' <span class="mw-changeslist-separator"></span> '; |
836 | $chardiff .= ChangesList::showCharacterDifference( |
837 | $parentLen, |
838 | $row->{$this->revisionLengthField}, |
839 | $this->getContext() |
840 | ); |
841 | $chardiff .= ' <span class="mw-changeslist-separator"></span> '; |
842 | } |
843 | return $chardiff; |
844 | } |
845 | |
846 | /** |
847 | * Format a comment for a revision. |
848 | * |
849 | * @param mixed $row |
850 | * @return string |
851 | */ |
852 | protected function formatComment( $row ) { |
853 | $comment = $this->formattedComments[$row->{$this->revisionIdField}]; |
854 | |
855 | if ( $comment === '' ) { |
856 | $defaultComment = $this->messages['changeslist-nocomment']; |
857 | $comment = "<span class=\"comment mw-comment-none\">$defaultComment</span>"; |
858 | } |
859 | |
860 | // Don't wrap result of this with <bdi> or any other element, see T377555 |
861 | return $comment; |
862 | } |
863 | |
864 | /** |
865 | * Format a user link. |
866 | * |
867 | * @param mixed $row |
868 | * @return string |
869 | */ |
870 | protected function formatUserLink( $row ) { |
871 | if ( !$this->currentRevRecord ) { |
872 | return ''; |
873 | } |
874 | $dir = $this->getLanguage()->getDir(); |
875 | |
876 | // When the author is different from the target, always show user and user talk links |
877 | $userlink = ''; |
878 | $revUser = $this->currentRevRecord->getUser(); |
879 | $revUserId = $revUser ? $revUser->getId() : 0; |
880 | $revUserText = $revUser ? $revUser->getName() : ''; |
881 | if ( $this->target !== $revUserText ) { |
882 | $userlink = ' <span class="mw-changeslist-separator"></span> ' |
883 | . Html::rawElement( 'bdi', [ 'dir' => $dir ], |
884 | Linker::userLink( $revUserId, $revUserText ) ); |
885 | $userlink .= ' ' . $this->msg( 'parentheses' )->rawParams( |
886 | Linker::userTalkLink( $revUserId, $revUserText ) )->escaped() . ' '; |
887 | } |
888 | return $userlink; |
889 | } |
890 | |
891 | /** |
892 | * @param mixed $row |
893 | * @return string[] |
894 | */ |
895 | protected function formatFlags( $row ) { |
896 | if ( !$this->currentRevRecord ) { |
897 | return []; |
898 | } |
899 | $flags = []; |
900 | if ( $this->currentRevRecord->getParentId() === 0 ) { |
901 | $flags[] = ChangesList::flag( 'newpage' ); |
902 | } |
903 | |
904 | if ( $this->currentRevRecord->isMinor() ) { |
905 | $flags[] = ChangesList::flag( 'minor' ); |
906 | } |
907 | return $flags; |
908 | } |
909 | |
910 | /** |
911 | * Format link for changing visibility. |
912 | * |
913 | * @param mixed $row |
914 | * @return string |
915 | */ |
916 | protected function formatVisibilityLink( $row ) { |
917 | if ( !$this->currentPage || !$this->currentRevRecord ) { |
918 | return ''; |
919 | } |
920 | $del = Linker::getRevDeleteLink( |
921 | $this->getAuthority(), |
922 | $this->currentRevRecord, |
923 | $this->currentPage |
924 | ); |
925 | if ( $del !== '' ) { |
926 | $del .= ' '; |
927 | } |
928 | return $del; |
929 | } |
930 | |
931 | /** |
932 | * @param mixed $row |
933 | * @param string[] &$classes |
934 | * @return string |
935 | */ |
936 | protected function formatTags( $row, &$classes ) { |
937 | # Tags, if any. Save some time using a cache. |
938 | [ $tagSummary, $newClasses ] = $this->tagsCache->getWithSetCallback( |
939 | $this->tagsCache->makeKey( |
940 | $row->ts_tags ?? '', |
941 | $this->getUser()->getName(), |
942 | $this->getLanguage()->getCode() |
943 | ), |
944 | fn () => ChangeTags::formatSummaryRow( |
945 | $row->ts_tags, |
946 | null, |
947 | $this->getContext() |
948 | ) |
949 | ); |
950 | $classes = array_merge( $classes, $newClasses ); |
951 | return $tagSummary; |
952 | } |
953 | |
954 | /** |
955 | * Check whether the revision author is deleted |
956 | * |
957 | * @param mixed $row |
958 | * @return bool |
959 | */ |
960 | public function revisionUserIsDeleted( $row ) { |
961 | return $this->currentRevRecord->isDeleted( RevisionRecord::DELETED_USER ); |
962 | } |
963 | |
964 | /** |
965 | * Generates each row in the contributions list. |
966 | * |
967 | * Contributions which are marked "top" are currently on top of the history. |
968 | * For these contributions, a [rollback] link is shown for users with roll- |
969 | * back privileges. The rollback link restores the most recent version that |
970 | * was not written by the target user. |
971 | * |
972 | * @todo This would probably look a lot nicer in a table. |
973 | * @param stdClass|mixed $row |
974 | * @return string |
975 | */ |
976 | public function formatRow( $row ) { |
977 | $ret = ''; |
978 | $classes = []; |
979 | $attribs = []; |
980 | |
981 | $this->currentPage = null; |
982 | $this->currentRevRecord = null; |
983 | |
984 | // Create a title for the revision if possible |
985 | // Rows from the hook may not include title information |
986 | if ( isset( $row->{$this->pageNamespaceField} ) && isset( $row->{$this->pageTitleField} ) ) { |
987 | $this->currentPage = Title::makeTitle( $row->{$this->pageNamespaceField}, $row->{$this->pageTitleField} ); |
988 | } |
989 | |
990 | // Flow overrides the ContribsPager::reallyDoQuery hook, causing this |
991 | // function to be called with a special object for $row. It expects us |
992 | // skip formatting so that the row can be formatted by the |
993 | // ContributionsLineEnding hook below. |
994 | // FIXME: have some better way for extensions to provide formatted rows. |
995 | $this->currentRevRecord = $this->tryCreatingRevisionRecord( $row, $this->currentPage ); |
996 | if ( $this->revisionsOnly || ( $this->currentRevRecord && $this->currentPage ) ) { |
997 | $this->populateAttributes( $row, $attribs ); |
998 | |
999 | $templateParams = $this->getTemplateParams( $row, $classes ); |
1000 | $ret = $this->getProcessedTemplate( $templateParams ); |
1001 | } |
1002 | |
1003 | // Let extensions add data |
1004 | $lineEndingsHook = $this->isArchive ? |
1005 | 'onDeletedContributionsLineEnding' : |
1006 | 'onContributionsLineEnding'; |
1007 | $this->hookRunner->$lineEndingsHook( $this, $ret, $row, $classes, $attribs ); |
1008 | $attribs = array_filter( $attribs, |
1009 | [ Sanitizer::class, 'isReservedDataAttribute' ], |
1010 | ARRAY_FILTER_USE_KEY |
1011 | ); |
1012 | |
1013 | // TODO: Handle exceptions in the catch block above. Do any extensions rely on |
1014 | // receiving empty rows? |
1015 | |
1016 | if ( $classes === [] && $attribs === [] && $ret === '' ) { |
1017 | wfDebug( "Dropping ContributionsSpecialPage row that could not be formatted" ); |
1018 | return "<!-- Could not format ContributionsSpecialPage row. -->\n"; |
1019 | } |
1020 | $attribs['class'] = $classes; |
1021 | |
1022 | // FIXME: The signature of the ContributionsLineEnding hook makes it |
1023 | // very awkward to move this LI wrapper into the template. |
1024 | return Html::rawElement( 'li', $attribs, $ret ) . "\n"; |
1025 | } |
1026 | |
1027 | /** |
1028 | * Generate array of template parameters to pass to the template for rendering. |
1029 | * Function can be overriden by classes to add/remove their own parameters. |
1030 | * |
1031 | * @since 1.43 |
1032 | * |
1033 | * @param stdClass|mixed $row |
1034 | * @param string[] &$classes |
1035 | * @return mixed[] |
1036 | */ |
1037 | public function getTemplateParams( $row, &$classes ) { |
1038 | $link = $this->formatArticleLink( $row ); |
1039 | $topmarktext = $this->formatTopMarkText( $row, $classes ); |
1040 | $diffHistLinks = $this->formatDiffHistLinks( $row ); |
1041 | $dateLink = $this->formatDateLink( $row ); |
1042 | $chardiff = $this->formatCharDiff( $row ); |
1043 | $comment = $this->formatComment( $row ); |
1044 | $userlink = $this->formatUserLink( $row ); |
1045 | $flags = $this->formatFlags( $row ); |
1046 | $del = $this->formatVisibilityLink( $row ); |
1047 | $tagSummary = $this->formatTags( $row, $classes ); |
1048 | |
1049 | if ( !$this->isArchive ) { |
1050 | $this->hookRunner->onSpecialContributions__formatRow__flags( |
1051 | $this->getContext(), $row, $flags ); |
1052 | } |
1053 | |
1054 | $templateParams = [ |
1055 | 'del' => $del, |
1056 | 'timestamp' => $dateLink, |
1057 | 'diffHistLinks' => $diffHistLinks, |
1058 | 'charDifference' => $chardiff, |
1059 | 'flags' => $flags, |
1060 | 'articleLink' => $link, |
1061 | 'userlink' => $userlink, |
1062 | 'logText' => $comment, |
1063 | 'topmarktext' => $topmarktext, |
1064 | 'tagSummary' => $tagSummary, |
1065 | ]; |
1066 | |
1067 | # Denote if username is redacted for this edit |
1068 | if ( $this->revisionUserIsDeleted( $row ) ) { |
1069 | $templateParams['rev-deleted-user-contribs'] = |
1070 | $this->msg( 'rev-deleted-user-contribs' )->escaped(); |
1071 | } |
1072 | |
1073 | return $templateParams; |
1074 | } |
1075 | |
1076 | /** |
1077 | * Return the processed template. Function can be overriden by classes |
1078 | * to provide their own template parser. |
1079 | * |
1080 | * @since 1.43 |
1081 | * |
1082 | * @param string[] $templateParams |
1083 | * @return string |
1084 | */ |
1085 | public function getProcessedTemplate( $templateParams ) { |
1086 | return $this->templateParser->processTemplate( |
1087 | 'SpecialContributionsLine', |
1088 | $templateParams |
1089 | ); |
1090 | } |
1091 | |
1092 | /** |
1093 | * Overwrite Pager function and return a helpful comment |
1094 | * @return string |
1095 | */ |
1096 | protected function getSqlComment() { |
1097 | if ( $this->namespace || $this->deletedOnly ) { |
1098 | // potentially slow, see CR r58153 |
1099 | return 'contributions page filtered for namespace or RevisionDeleted edits'; |
1100 | } else { |
1101 | return 'contributions page unfiltered'; |
1102 | } |
1103 | } |
1104 | |
1105 | /** |
1106 | * @deprecated since 1.38, use ::setPreventClickjacking() instead |
1107 | */ |
1108 | protected function preventClickjacking() { |
1109 | $this->setPreventClickjacking( true ); |
1110 | } |
1111 | |
1112 | /** |
1113 | * @param bool $enable |
1114 | * @since 1.38 |
1115 | */ |
1116 | protected function setPreventClickjacking( bool $enable ) { |
1117 | $this->preventClickjacking = $enable; |
1118 | } |
1119 | |
1120 | /** |
1121 | * @return bool |
1122 | */ |
1123 | public function getPreventClickjacking() { |
1124 | return $this->preventClickjacking; |
1125 | } |
1126 | |
1127 | } |