This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * Page history |
||
4 | * |
||
5 | * Split off from Article.php and Skin.php, 2003-12-22 |
||
6 | * |
||
7 | * This program is free software; you can redistribute it and/or modify |
||
8 | * it under the terms of the GNU General Public License as published by |
||
9 | * the Free Software Foundation; either version 2 of the License, or |
||
10 | * (at your option) any later version. |
||
11 | * |
||
12 | * This program is distributed in the hope that it will be useful, |
||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
15 | * GNU General Public License for more details. |
||
16 | * |
||
17 | * You should have received a copy of the GNU General Public License along |
||
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
20 | * http://www.gnu.org/copyleft/gpl.html |
||
21 | * |
||
22 | * @file |
||
23 | * @ingroup Actions |
||
24 | */ |
||
25 | |||
26 | /** |
||
27 | * This class handles printing the history page for an article. In order to |
||
28 | * be efficient, it uses timestamps rather than offsets for paging, to avoid |
||
29 | * costly LIMIT,offset queries. |
||
30 | * |
||
31 | * Construct it by passing in an Article, and call $h->history() to print the |
||
32 | * history. |
||
33 | * |
||
34 | * @ingroup Actions |
||
35 | */ |
||
36 | class HistoryAction extends FormlessAction { |
||
37 | const DIR_PREV = 0; |
||
38 | const DIR_NEXT = 1; |
||
39 | |||
40 | /** @var array Array of message keys and strings */ |
||
41 | public $message; |
||
42 | |||
43 | public function getName() { |
||
44 | return 'history'; |
||
45 | } |
||
46 | |||
47 | public function requiresWrite() { |
||
48 | return false; |
||
49 | } |
||
50 | |||
51 | public function requiresUnblock() { |
||
52 | return false; |
||
53 | } |
||
54 | |||
55 | protected function getPageTitle() { |
||
56 | return $this->msg( 'history-title', $this->getTitle()->getPrefixedText() )->text(); |
||
57 | } |
||
58 | |||
59 | protected function getDescription() { |
||
60 | // Creation of a subtitle link pointing to [[Special:Log]] |
||
61 | return Linker::linkKnown( |
||
62 | SpecialPage::getTitleFor( 'Log' ), |
||
63 | $this->msg( 'viewpagelogs' )->escaped(), |
||
64 | [], |
||
65 | [ 'page' => $this->getTitle()->getPrefixedText() ] |
||
66 | ); |
||
67 | } |
||
68 | |||
69 | /** |
||
70 | * @return WikiPage|Article|ImagePage|CategoryPage|Page The Article object we are working on. |
||
71 | */ |
||
72 | public function getArticle() { |
||
73 | return $this->page; |
||
74 | } |
||
75 | |||
76 | /** |
||
77 | * As we use the same small set of messages in various methods and that |
||
78 | * they are called often, we call them once and save them in $this->message |
||
79 | */ |
||
80 | private function preCacheMessages() { |
||
81 | // Precache various messages |
||
82 | if ( !isset( $this->message ) ) { |
||
83 | $msgs = [ 'cur', 'last', 'pipe-separator' ]; |
||
84 | foreach ( $msgs as $msg ) { |
||
85 | $this->message[$msg] = $this->msg( $msg )->escaped(); |
||
86 | } |
||
87 | } |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Print the history page for an article. |
||
92 | */ |
||
93 | function onView() { |
||
94 | $out = $this->getOutput(); |
||
95 | $request = $this->getRequest(); |
||
96 | |||
97 | /** |
||
98 | * Allow client caching. |
||
99 | */ |
||
100 | if ( $out->checkLastModified( $this->page->getTouched() ) ) { |
||
101 | return; // Client cache fresh and headers sent, nothing more to do. |
||
102 | } |
||
103 | |||
104 | $this->preCacheMessages(); |
||
105 | $config = $this->context->getConfig(); |
||
106 | |||
107 | # Fill in the file cache if not set already |
||
108 | if ( HTMLFileCache::useFileCache( $this->getContext() ) ) { |
||
109 | $cache = new HTMLFileCache( $this->getTitle(), 'history' ); |
||
110 | if ( !$cache->isCacheGood( /* Assume up to date */ ) ) { |
||
111 | ob_start( [ &$cache, 'saveToFileCache' ] ); |
||
112 | } |
||
113 | } |
||
114 | |||
115 | // Setup page variables. |
||
116 | $out->setFeedAppendQuery( 'action=history' ); |
||
117 | $out->addModules( 'mediawiki.action.history' ); |
||
118 | $out->addModuleStyles( [ |
||
119 | 'mediawiki.action.history.styles', |
||
120 | 'mediawiki.special.changeslist', |
||
121 | ] ); |
||
122 | if ( $config->get( 'UseMediaWikiUIEverywhere' ) ) { |
||
123 | $out = $this->getOutput(); |
||
124 | $out->addModuleStyles( [ |
||
125 | 'mediawiki.ui.input', |
||
126 | 'mediawiki.ui.checkbox', |
||
127 | ] ); |
||
128 | } |
||
129 | |||
130 | // Handle atom/RSS feeds. |
||
131 | $feedType = $request->getVal( 'feed' ); |
||
132 | if ( $feedType ) { |
||
133 | $this->feed( $feedType ); |
||
134 | |||
135 | return; |
||
136 | } |
||
137 | |||
138 | $this->addHelpLink( '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Page_history', true ); |
||
139 | |||
140 | // Fail nicely if article doesn't exist. |
||
141 | if ( !$this->page->exists() ) { |
||
142 | global $wgSend404Code; |
||
143 | if ( $wgSend404Code ) { |
||
144 | $out->setStatusCode( 404 ); |
||
145 | } |
||
146 | $out->addWikiMsg( 'nohistory' ); |
||
147 | # show deletion/move log if there is an entry |
||
148 | LogEventsList::showLogExtract( |
||
149 | $out, |
||
150 | [ 'delete', 'move' ], |
||
151 | $this->getTitle(), |
||
152 | '', |
||
153 | [ 'lim' => 10, |
||
154 | 'conds' => [ "log_action != 'revision'" ], |
||
155 | 'showIfEmpty' => false, |
||
156 | 'msgKey' => [ 'moveddeleted-notice' ] |
||
157 | ] |
||
158 | ); |
||
159 | |||
160 | return; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Add date selector to quickly get to a certain time |
||
165 | */ |
||
166 | $year = $request->getInt( 'year' ); |
||
167 | $month = $request->getInt( 'month' ); |
||
168 | $tagFilter = $request->getVal( 'tagfilter' ); |
||
169 | $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter ); |
||
170 | |||
171 | /** |
||
172 | * Option to show only revisions that have been (partially) hidden via RevisionDelete |
||
173 | */ |
||
174 | if ( $request->getBool( 'deleted' ) ) { |
||
175 | $conds = [ 'rev_deleted != 0' ]; |
||
176 | } else { |
||
177 | $conds = []; |
||
178 | } |
||
179 | if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { |
||
180 | $checkDeleted = Xml::checkLabel( $this->msg( 'history-show-deleted' )->text(), |
||
181 | 'deleted', 'mw-show-deleted-only', $request->getBool( 'deleted' ) ) . "\n"; |
||
182 | } else { |
||
183 | $checkDeleted = ''; |
||
184 | } |
||
185 | |||
186 | // Add the general form |
||
187 | $action = htmlspecialchars( wfScript() ); |
||
188 | $out->addHTML( |
||
189 | "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" . |
||
190 | Xml::fieldset( |
||
191 | $this->msg( 'history-fieldset-title' )->text(), |
||
192 | false, |
||
193 | [ 'id' => 'mw-history-search' ] |
||
194 | ) . |
||
195 | Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n" . |
||
196 | Html::hidden( 'action', 'history' ) . "\n" . |
||
197 | Xml::dateMenu( |
||
198 | ( $year == null ? MWTimestamp::getLocalInstance()->format( 'Y' ) : $year ), |
||
199 | $month |
||
200 | ) . ' ' . |
||
201 | ( $tagSelector ? ( implode( ' ', $tagSelector ) . ' ' ) : '' ) . |
||
202 | $checkDeleted . |
||
203 | Html::submitButton( |
||
204 | $this->msg( 'historyaction-submit' )->text(), |
||
205 | [], |
||
206 | [ 'mw-ui-progressive' ] |
||
207 | ) . "\n" . |
||
208 | '</fieldset></form>' |
||
209 | ); |
||
210 | |||
211 | Hooks::run( 'PageHistoryBeforeList', [ &$this->page, $this->getContext() ] ); |
||
212 | |||
213 | // Create and output the list. |
||
214 | $pager = new HistoryPager( $this, $year, $month, $tagFilter, $conds ); |
||
215 | $out->addHTML( |
||
216 | $pager->getNavigationBar() . |
||
217 | $pager->getBody() . |
||
218 | $pager->getNavigationBar() |
||
219 | ); |
||
220 | $out->preventClickjacking( $pager->getPreventClickjacking() ); |
||
221 | } |
||
222 | |||
223 | /** |
||
224 | * Fetch an array of revisions, specified by a given limit, offset and |
||
225 | * direction. This is now only used by the feeds. It was previously |
||
226 | * used by the main UI but that's now handled by the pager. |
||
227 | * |
||
228 | * @param int $limit The limit number of revisions to get |
||
229 | * @param int $offset |
||
230 | * @param int $direction Either self::DIR_PREV or self::DIR_NEXT |
||
231 | * @return ResultWrapper |
||
232 | */ |
||
233 | function fetchRevisions( $limit, $offset, $direction ) { |
||
234 | // Fail if article doesn't exist. |
||
235 | if ( !$this->getTitle()->exists() ) { |
||
236 | return new FakeResultWrapper( [] ); |
||
237 | } |
||
238 | |||
239 | $dbr = wfGetDB( DB_REPLICA ); |
||
240 | |||
241 | if ( $direction === self::DIR_PREV ) { |
||
242 | list( $dirs, $oper ) = [ "ASC", ">=" ]; |
||
243 | } else { /* $direction === self::DIR_NEXT */ |
||
244 | list( $dirs, $oper ) = [ "DESC", "<=" ]; |
||
245 | } |
||
246 | |||
247 | if ( $offset ) { |
||
248 | $offsets = [ "rev_timestamp $oper " . $dbr->addQuotes( $dbr->timestamp( $offset ) ) ]; |
||
249 | } else { |
||
250 | $offsets = []; |
||
251 | } |
||
252 | |||
253 | $page_id = $this->page->getId(); |
||
254 | |||
255 | return $dbr->select( 'revision', |
||
256 | Revision::selectFields(), |
||
257 | array_merge( [ 'rev_page' => $page_id ], $offsets ), |
||
258 | __METHOD__, |
||
259 | [ 'ORDER BY' => "rev_timestamp $dirs", |
||
260 | 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit ] |
||
261 | ); |
||
262 | } |
||
263 | |||
264 | /** |
||
265 | * Output a subscription feed listing recent edits to this page. |
||
266 | * |
||
267 | * @param string $type Feed type |
||
268 | */ |
||
269 | function feed( $type ) { |
||
270 | if ( !FeedUtils::checkFeedOutput( $type ) ) { |
||
271 | return; |
||
272 | } |
||
273 | $request = $this->getRequest(); |
||
274 | |||
275 | $feedClasses = $this->context->getConfig()->get( 'FeedClasses' ); |
||
276 | /** @var RSSFeed|AtomFeed $feed */ |
||
277 | $feed = new $feedClasses[$type]( |
||
278 | $this->getTitle()->getPrefixedText() . ' - ' . |
||
279 | $this->msg( 'history-feed-title' )->inContentLanguage()->text(), |
||
280 | $this->msg( 'history-feed-description' )->inContentLanguage()->text(), |
||
281 | $this->getTitle()->getFullURL( 'action=history' ) |
||
282 | ); |
||
283 | |||
284 | // Get a limit on number of feed entries. Provide a sane default |
||
285 | // of 10 if none is defined (but limit to $wgFeedLimit max) |
||
286 | $limit = $request->getInt( 'limit', 10 ); |
||
287 | $limit = min( |
||
288 | max( $limit, 1 ), |
||
289 | $this->context->getConfig()->get( 'FeedLimit' ) |
||
290 | ); |
||
291 | |||
292 | $items = $this->fetchRevisions( $limit, 0, self::DIR_NEXT ); |
||
293 | |||
294 | // Generate feed elements enclosed between header and footer. |
||
295 | $feed->outHeader(); |
||
296 | if ( $items->numRows() ) { |
||
297 | foreach ( $items as $row ) { |
||
298 | $feed->outItem( $this->feedItem( $row ) ); |
||
0 ignored issues
–
show
|
|||
299 | } |
||
300 | } else { |
||
301 | $feed->outItem( $this->feedEmpty() ); |
||
302 | } |
||
303 | $feed->outFooter(); |
||
304 | } |
||
305 | |||
306 | function feedEmpty() { |
||
307 | return new FeedItem( |
||
308 | $this->msg( 'nohistory' )->inContentLanguage()->text(), |
||
309 | $this->msg( 'history-feed-empty' )->inContentLanguage()->parseAsBlock(), |
||
310 | $this->getTitle()->getFullURL(), |
||
311 | wfTimestamp( TS_MW ), |
||
0 ignored issues
–
show
|
|||
312 | '', |
||
313 | $this->getTitle()->getTalkPage()->getFullURL() |
||
314 | ); |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Generate a FeedItem object from a given revision table row |
||
319 | * Borrows Recent Changes' feed generation functions for formatting; |
||
320 | * includes a diff to the previous revision (if any). |
||
321 | * |
||
322 | * @param stdClass|array $row Database row |
||
323 | * @return FeedItem |
||
324 | */ |
||
325 | function feedItem( $row ) { |
||
326 | $rev = new Revision( $row ); |
||
327 | $rev->setTitle( $this->getTitle() ); |
||
328 | $text = FeedUtils::formatDiffRow( |
||
329 | $this->getTitle(), |
||
330 | $this->getTitle()->getPreviousRevisionID( $rev->getId() ), |
||
331 | $rev->getId(), |
||
332 | $rev->getTimestamp(), |
||
333 | $rev->getComment() |
||
334 | ); |
||
335 | if ( $rev->getComment() == '' ) { |
||
336 | global $wgContLang; |
||
337 | $title = $this->msg( 'history-feed-item-nocomment', |
||
338 | $rev->getUserText(), |
||
339 | $wgContLang->timeanddate( $rev->getTimestamp() ), |
||
340 | $wgContLang->date( $rev->getTimestamp() ), |
||
341 | $wgContLang->time( $rev->getTimestamp() ) )->inContentLanguage()->text(); |
||
342 | } else { |
||
343 | $title = $rev->getUserText() . |
||
344 | $this->msg( 'colon-separator' )->inContentLanguage()->text() . |
||
345 | FeedItem::stripComment( $rev->getComment() ); |
||
346 | } |
||
347 | |||
348 | return new FeedItem( |
||
349 | $title, |
||
350 | $text, |
||
351 | $this->getTitle()->getFullURL( 'diff=' . $rev->getId() . '&oldid=prev' ), |
||
352 | $rev->getTimestamp(), |
||
0 ignored issues
–
show
|
|||
353 | $rev->getUserText(), |
||
0 ignored issues
–
show
It seems like
$rev->getUserText() targeting Revision::getUserText() can also be of type boolean ; however, FeedItem::__construct() does only seem to accept string , maybe add an additional type check?
This check looks at variables that are passed out again to other methods. If the outgoing method call has stricter type requirements than the method itself, an issue is raised. An additional type check may prevent trouble.
Loading history...
|
|||
354 | $this->getTitle()->getTalkPage()->getFullURL() |
||
355 | ); |
||
356 | } |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * @ingroup Pager |
||
361 | * @ingroup Actions |
||
362 | */ |
||
363 | class HistoryPager extends ReverseChronologicalPager { |
||
364 | /** |
||
365 | * @var bool|stdClass |
||
366 | */ |
||
367 | public $lastRow = false; |
||
368 | |||
369 | public $counter, $historyPage, $buttons, $conds; |
||
370 | |||
371 | protected $oldIdChecked; |
||
372 | |||
373 | protected $preventClickjacking = false; |
||
374 | /** |
||
375 | * @var array |
||
376 | */ |
||
377 | protected $parentLens; |
||
378 | |||
379 | /** @var bool Whether to show the tag editing UI */ |
||
380 | protected $showTagEditUI; |
||
381 | |||
382 | /** |
||
383 | * @param HistoryAction $historyPage |
||
384 | * @param string $year |
||
385 | * @param string $month |
||
386 | * @param string $tagFilter |
||
387 | * @param array $conds |
||
388 | */ |
||
389 | function __construct( $historyPage, $year = '', $month = '', $tagFilter = '', $conds = [] ) { |
||
390 | parent::__construct( $historyPage->getContext() ); |
||
391 | $this->historyPage = $historyPage; |
||
392 | $this->tagFilter = $tagFilter; |
||
0 ignored issues
–
show
The property
tagFilter does not exist. Did you maybe forget to declare it?
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code: class MyClass { }
$x = new MyClass();
$x->foo = true;
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: class MyClass {
public $foo;
}
$x = new MyClass();
$x->foo = true;
Loading history...
|
|||
393 | $this->getDateCond( $year, $month ); |
||
394 | $this->conds = $conds; |
||
395 | $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getUser() ); |
||
396 | } |
||
397 | |||
398 | // For hook compatibility... |
||
399 | function getArticle() { |
||
400 | return $this->historyPage->getArticle(); |
||
401 | } |
||
402 | |||
403 | function getSqlComment() { |
||
404 | if ( $this->conds ) { |
||
405 | return 'history page filtered'; // potentially slow, see CR r58153 |
||
406 | } else { |
||
407 | return 'history page unfiltered'; |
||
408 | } |
||
409 | } |
||
410 | |||
411 | function getQueryInfo() { |
||
412 | $queryInfo = [ |
||
413 | 'tables' => [ 'revision', 'user' ], |
||
414 | 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), |
||
415 | 'conds' => array_merge( |
||
416 | [ 'rev_page' => $this->getWikiPage()->getId() ], |
||
417 | $this->conds ), |
||
418 | 'options' => [ 'USE INDEX' => [ 'revision' => 'page_timestamp' ] ], |
||
419 | 'join_conds' => [ 'user' => Revision::userJoinCond() ], |
||
420 | ]; |
||
421 | ChangeTags::modifyDisplayQuery( |
||
422 | $queryInfo['tables'], |
||
423 | $queryInfo['fields'], |
||
424 | $queryInfo['conds'], |
||
425 | $queryInfo['join_conds'], |
||
426 | $queryInfo['options'], |
||
427 | $this->tagFilter |
||
428 | ); |
||
429 | Hooks::run( 'PageHistoryPager::getQueryInfo', [ &$this, &$queryInfo ] ); |
||
430 | |||
431 | return $queryInfo; |
||
432 | } |
||
433 | |||
434 | function getIndexField() { |
||
435 | return 'rev_timestamp'; |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * @param stdClass $row |
||
440 | * @return string |
||
441 | */ |
||
442 | function formatRow( $row ) { |
||
443 | if ( $this->lastRow ) { |
||
444 | $latest = ( $this->counter == 1 && $this->mIsFirst ); |
||
445 | $firstInList = $this->counter == 1; |
||
446 | $this->counter++; |
||
447 | |||
448 | $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' ) |
||
449 | ? $this->getTitle()->getNotificationTimestamp( $this->getUser() ) |
||
450 | : false; |
||
451 | |||
452 | $s = $this->historyLine( |
||
453 | $this->lastRow, $row, $notifTimestamp, $latest, $firstInList ); |
||
454 | } else { |
||
455 | $s = ''; |
||
456 | } |
||
457 | $this->lastRow = $row; |
||
458 | |||
459 | return $s; |
||
460 | } |
||
461 | |||
462 | function doBatchLookups() { |
||
463 | if ( !Hooks::run( 'PageHistoryPager::doBatchLookups', [ $this, $this->mResult ] ) ) { |
||
464 | return; |
||
465 | } |
||
466 | |||
467 | # Do a link batch query |
||
468 | $this->mResult->seek( 0 ); |
||
469 | $batch = new LinkBatch(); |
||
470 | $revIds = []; |
||
471 | foreach ( $this->mResult as $row ) { |
||
472 | if ( $row->rev_parent_id ) { |
||
473 | $revIds[] = $row->rev_parent_id; |
||
474 | } |
||
475 | if ( !is_null( $row->user_name ) ) { |
||
476 | $batch->add( NS_USER, $row->user_name ); |
||
477 | $batch->add( NS_USER_TALK, $row->user_name ); |
||
478 | } else { # for anons or usernames of imported revisions |
||
479 | $batch->add( NS_USER, $row->rev_user_text ); |
||
480 | $batch->add( NS_USER_TALK, $row->rev_user_text ); |
||
481 | } |
||
482 | } |
||
483 | $this->parentLens = Revision::getParentLengths( $this->mDb, $revIds ); |
||
484 | $batch->execute(); |
||
485 | $this->mResult->seek( 0 ); |
||
486 | } |
||
487 | |||
488 | /** |
||
489 | * Creates begin of history list with a submit button |
||
490 | * |
||
491 | * @return string HTML output |
||
492 | */ |
||
493 | function getStartBody() { |
||
494 | $this->lastRow = false; |
||
495 | $this->counter = 1; |
||
496 | $this->oldIdChecked = 0; |
||
497 | |||
498 | $this->getOutput()->wrapWikiMsg( "<div class='mw-history-legend'>\n$1\n</div>", 'histlegend' ); |
||
499 | $s = Html::openElement( 'form', [ 'action' => wfScript(), |
||
500 | 'id' => 'mw-history-compare' ] ) . "\n"; |
||
501 | $s .= Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) . "\n"; |
||
502 | $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; |
||
503 | $s .= Html::hidden( 'type', 'revision' ) . "\n"; |
||
504 | |||
505 | // Button container stored in $this->buttons for re-use in getEndBody() |
||
506 | $this->buttons = '<div>'; |
||
507 | $className = 'historysubmit mw-history-compareselectedversions-button'; |
||
508 | $attrs = [ 'class' => $className ] |
||
509 | + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' ); |
||
510 | $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(), |
||
511 | $attrs |
||
512 | ) . "\n"; |
||
513 | |||
514 | $user = $this->getUser(); |
||
515 | $actionButtons = ''; |
||
516 | if ( $user->isAllowed( 'deleterevision' ) ) { |
||
517 | $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' ); |
||
518 | } |
||
519 | if ( $this->showTagEditUI ) { |
||
520 | $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' ); |
||
521 | } |
||
522 | if ( $actionButtons ) { |
||
523 | $this->buttons .= Xml::tags( 'div', [ 'class' => |
||
524 | 'mw-history-revisionactions' ], $actionButtons ); |
||
525 | } |
||
526 | |||
527 | if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) { |
||
528 | $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); |
||
529 | } |
||
530 | |||
531 | $this->buttons .= '</div>'; |
||
532 | |||
533 | $s .= $this->buttons; |
||
534 | $s .= '<ul id="pagehistory">' . "\n"; |
||
535 | |||
536 | return $s; |
||
537 | } |
||
538 | |||
539 | private function getRevisionButton( $name, $msg ) { |
||
540 | $this->preventClickjacking(); |
||
541 | # Note bug #20966, <button> is non-standard in IE<8 |
||
542 | $element = Html::element( |
||
543 | 'button', |
||
544 | [ |
||
545 | 'type' => 'submit', |
||
546 | 'name' => $name, |
||
547 | 'value' => '1', |
||
548 | 'class' => "historysubmit mw-history-$name-button", |
||
549 | ], |
||
550 | $this->msg( $msg )->text() |
||
551 | ) . "\n"; |
||
552 | return $element; |
||
553 | } |
||
554 | |||
555 | function getEndBody() { |
||
556 | if ( $this->lastRow ) { |
||
557 | $latest = $this->counter == 1 && $this->mIsFirst; |
||
558 | $firstInList = $this->counter == 1; |
||
559 | if ( $this->mIsBackwards ) { |
||
560 | # Next row is unknown, but for UI reasons, probably exists if an offset has been specified |
||
561 | if ( $this->mOffset == '' ) { |
||
562 | $next = null; |
||
563 | } else { |
||
564 | $next = 'unknown'; |
||
565 | } |
||
566 | } else { |
||
567 | # The next row is the past-the-end row |
||
568 | $next = $this->mPastTheEndRow; |
||
569 | } |
||
570 | $this->counter++; |
||
571 | |||
572 | $notifTimestamp = $this->getConfig()->get( 'ShowUpdatedMarker' ) |
||
573 | ? $this->getTitle()->getNotificationTimestamp( $this->getUser() ) |
||
574 | : false; |
||
575 | |||
576 | $s = $this->historyLine( |
||
577 | $this->lastRow, $next, $notifTimestamp, $latest, $firstInList ); |
||
578 | } else { |
||
579 | $s = ''; |
||
580 | } |
||
581 | $s .= "</ul>\n"; |
||
582 | # Add second buttons only if there is more than one rev |
||
583 | if ( $this->getNumRows() > 2 ) { |
||
584 | $s .= $this->buttons; |
||
585 | } |
||
586 | $s .= '</form>'; |
||
587 | |||
588 | return $s; |
||
589 | } |
||
590 | |||
591 | /** |
||
592 | * Creates a submit button |
||
593 | * |
||
594 | * @param string $message Text of the submit button, will be escaped |
||
595 | * @param array $attributes Attributes |
||
596 | * @return string HTML output for the submit button |
||
597 | */ |
||
598 | function submitButton( $message, $attributes = [] ) { |
||
599 | # Disable submit button if history has 1 revision only |
||
600 | if ( $this->getNumRows() > 1 ) { |
||
601 | return Html::submitButton( $message, $attributes ); |
||
602 | } else { |
||
603 | return ''; |
||
604 | } |
||
605 | } |
||
606 | |||
607 | /** |
||
608 | * Returns a row from the history printout. |
||
609 | * |
||
610 | * @todo document some more, and maybe clean up the code (some params redundant?) |
||
611 | * |
||
612 | * @param stdClass $row The database row corresponding to the previous line. |
||
613 | * @param mixed $next The database row corresponding to the next line |
||
614 | * (chronologically previous) |
||
615 | * @param bool|string $notificationtimestamp |
||
616 | * @param bool $latest Whether this row corresponds to the page's latest revision. |
||
617 | * @param bool $firstInList Whether this row corresponds to the first |
||
618 | * displayed on this history page. |
||
619 | * @return string HTML output for the row |
||
620 | */ |
||
621 | function historyLine( $row, $next, $notificationtimestamp = false, |
||
622 | $latest = false, $firstInList = false ) { |
||
623 | $rev = new Revision( $row ); |
||
624 | $rev->setTitle( $this->getTitle() ); |
||
625 | |||
626 | if ( is_object( $next ) ) { |
||
627 | $prevRev = new Revision( $next ); |
||
628 | $prevRev->setTitle( $this->getTitle() ); |
||
629 | } else { |
||
630 | $prevRev = null; |
||
631 | } |
||
632 | |||
633 | $curlink = $this->curLink( $rev, $latest ); |
||
634 | $lastlink = $this->lastLink( $rev, $next ); |
||
635 | $curLastlinks = $curlink . $this->historyPage->message['pipe-separator'] . $lastlink; |
||
636 | $histLinks = Html::rawElement( |
||
637 | 'span', |
||
638 | [ 'class' => 'mw-history-histlinks' ], |
||
639 | $this->msg( 'parentheses' )->rawParams( $curLastlinks )->escaped() |
||
640 | ); |
||
641 | |||
642 | $diffButtons = $this->diffButtons( $rev, $firstInList ); |
||
643 | $s = $histLinks . $diffButtons; |
||
644 | |||
645 | $link = $this->revLink( $rev ); |
||
646 | $classes = []; |
||
647 | |||
648 | $del = ''; |
||
649 | $user = $this->getUser(); |
||
650 | $canRevDelete = $user->isAllowed( 'deleterevision' ); |
||
651 | // Show checkboxes for each revision, to allow for revision deletion and |
||
652 | // change tags |
||
653 | if ( $canRevDelete || $this->showTagEditUI ) { |
||
654 | $this->preventClickjacking(); |
||
655 | // If revision was hidden from sysops and we don't need the checkbox |
||
656 | // for anything else, disable it |
||
657 | View Code Duplication | if ( !$this->showTagEditUI && !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { |
|
658 | $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); |
||
659 | // Otherwise, enable the checkbox... |
||
660 | } else { |
||
661 | $del = Xml::check( 'showhiderevisions', false, |
||
662 | [ 'name' => 'ids[' . $rev->getId() . ']' ] ); |
||
663 | } |
||
664 | // User can only view deleted revisions... |
||
665 | } elseif ( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) { |
||
666 | // If revision was hidden from sysops, disable the link |
||
667 | if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) { |
||
668 | $del = Linker::revDeleteLinkDisabled( false ); |
||
669 | // Otherwise, show the link... |
||
670 | } else { |
||
671 | $query = [ 'type' => 'revision', |
||
672 | 'target' => $this->getTitle()->getPrefixedDBkey(), 'ids' => $rev->getId() ]; |
||
673 | $del .= Linker::revDeleteLink( $query, |
||
674 | $rev->isDeleted( Revision::DELETED_RESTRICTED ), false ); |
||
675 | } |
||
676 | } |
||
677 | if ( $del ) { |
||
678 | $s .= " $del "; |
||
679 | } |
||
680 | |||
681 | $lang = $this->getLanguage(); |
||
682 | $dirmark = $lang->getDirMark(); |
||
683 | |||
684 | $s .= " $link"; |
||
685 | $s .= $dirmark; |
||
686 | $s .= " <span class='history-user'>" . |
||
687 | Linker::revUserTools( $rev, true ) . "</span>"; |
||
688 | $s .= $dirmark; |
||
689 | |||
690 | if ( $rev->isMinor() ) { |
||
691 | $s .= ' ' . ChangesList::flag( 'minor', $this->getContext() ); |
||
692 | } |
||
693 | |||
694 | # Sometimes rev_len isn't populated |
||
695 | if ( $rev->getSize() !== null ) { |
||
696 | # Size is always public data |
||
697 | $prevSize = isset( $this->parentLens[$row->rev_parent_id] ) |
||
698 | ? $this->parentLens[$row->rev_parent_id] |
||
699 | : 0; |
||
700 | $sDiff = ChangesList::showCharacterDifference( $prevSize, $rev->getSize() ); |
||
701 | $fSize = Linker::formatRevisionSize( $rev->getSize() ); |
||
702 | $s .= ' <span class="mw-changeslist-separator">. .</span> ' . "$fSize $sDiff"; |
||
703 | } |
||
704 | |||
705 | # Text following the character difference is added just before running hooks |
||
706 | $s2 = Linker::revComment( $rev, false, true ); |
||
707 | |||
708 | if ( $notificationtimestamp && ( $row->rev_timestamp >= $notificationtimestamp ) ) { |
||
709 | $s2 .= ' <span class="updatedmarker">' . $this->msg( 'updatedmarker' )->escaped() . '</span>'; |
||
710 | $classes[] = 'mw-history-line-updated'; |
||
711 | } |
||
712 | |||
713 | $tools = []; |
||
714 | |||
715 | # Rollback and undo links |
||
716 | if ( $prevRev && $this->getTitle()->quickUserCan( 'edit', $user ) ) { |
||
717 | if ( $latest && $this->getTitle()->quickUserCan( 'rollback', $user ) ) { |
||
718 | // Get a rollback link without the brackets |
||
719 | $rollbackLink = Linker::generateRollback( |
||
720 | $rev, |
||
721 | $this->getContext(), |
||
722 | [ 'verify', 'noBrackets' ] |
||
723 | ); |
||
724 | if ( $rollbackLink ) { |
||
725 | $this->preventClickjacking(); |
||
726 | $tools[] = $rollbackLink; |
||
727 | } |
||
728 | } |
||
729 | |||
730 | if ( !$rev->isDeleted( Revision::DELETED_TEXT ) |
||
731 | && !$prevRev->isDeleted( Revision::DELETED_TEXT ) |
||
732 | ) { |
||
733 | # Create undo tooltip for the first (=latest) line only |
||
734 | $undoTooltip = $latest |
||
735 | ? [ 'title' => $this->msg( 'tooltip-undo' )->text() ] |
||
736 | : []; |
||
737 | $undolink = Linker::linkKnown( |
||
738 | $this->getTitle(), |
||
739 | $this->msg( 'editundo' )->escaped(), |
||
740 | $undoTooltip, |
||
741 | [ |
||
742 | 'action' => 'edit', |
||
743 | 'undoafter' => $prevRev->getId(), |
||
744 | 'undo' => $rev->getId() |
||
745 | ] |
||
746 | ); |
||
747 | $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>"; |
||
748 | } |
||
749 | } |
||
750 | // Allow extension to add their own links here |
||
751 | Hooks::run( 'HistoryRevisionTools', [ $rev, &$tools, $prevRev, $user ] ); |
||
752 | |||
753 | if ( $tools ) { |
||
754 | $s2 .= ' ' . $this->msg( 'parentheses' )->rawParams( $lang->pipeList( $tools ) )->escaped(); |
||
755 | } |
||
756 | |||
757 | # Tags |
||
758 | list( $tagSummary, $newClasses ) = ChangeTags::formatSummaryRow( |
||
759 | $row->ts_tags, |
||
760 | 'history', |
||
761 | $this->getContext() |
||
762 | ); |
||
763 | $classes = array_merge( $classes, $newClasses ); |
||
764 | if ( $tagSummary !== '' ) { |
||
765 | $s2 .= " $tagSummary"; |
||
766 | } |
||
767 | |||
768 | # Include separator between character difference and following text |
||
769 | if ( $s2 !== '' ) { |
||
770 | $s .= ' <span class="mw-changeslist-separator">. .</span> ' . $s2; |
||
771 | } |
||
772 | |||
773 | Hooks::run( 'PageHistoryLineEnding', [ $this, &$row, &$s, &$classes ] ); |
||
774 | |||
775 | $attribs = []; |
||
776 | if ( $classes ) { |
||
777 | $attribs['class'] = implode( ' ', $classes ); |
||
778 | } |
||
779 | |||
780 | return Xml::tags( 'li', $attribs, $s ) . "\n"; |
||
781 | } |
||
782 | |||
783 | /** |
||
784 | * Create a link to view this revision of the page |
||
785 | * |
||
786 | * @param Revision $rev |
||
787 | * @return string |
||
788 | */ |
||
789 | function revLink( $rev ) { |
||
790 | $date = $this->getLanguage()->userTimeAndDate( $rev->getTimestamp(), $this->getUser() ); |
||
791 | $date = htmlspecialchars( $date ); |
||
792 | if ( $rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { |
||
793 | $link = Linker::linkKnown( |
||
794 | $this->getTitle(), |
||
795 | $date, |
||
796 | [ 'class' => 'mw-changeslist-date' ], |
||
797 | [ 'oldid' => $rev->getId() ] |
||
798 | ); |
||
799 | } else { |
||
800 | $link = $date; |
||
801 | } |
||
802 | if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { |
||
803 | $link = "<span class=\"history-deleted\">$link</span>"; |
||
804 | } |
||
805 | |||
806 | return $link; |
||
807 | } |
||
808 | |||
809 | /** |
||
810 | * Create a diff-to-current link for this revision for this page |
||
811 | * |
||
812 | * @param Revision $rev |
||
813 | * @param bool $latest This is the latest revision of the page? |
||
814 | * @return string |
||
815 | */ |
||
816 | function curLink( $rev, $latest ) { |
||
817 | $cur = $this->historyPage->message['cur']; |
||
818 | if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { |
||
819 | return $cur; |
||
820 | } else { |
||
821 | return Linker::linkKnown( |
||
822 | $this->getTitle(), |
||
823 | $cur, |
||
824 | [], |
||
825 | [ |
||
826 | 'diff' => $this->getWikiPage()->getLatest(), |
||
827 | 'oldid' => $rev->getId() |
||
828 | ] |
||
829 | ); |
||
830 | } |
||
831 | } |
||
832 | |||
833 | /** |
||
834 | * Create a diff-to-previous link for this revision for this page. |
||
835 | * |
||
836 | * @param Revision $prevRev The revision being displayed |
||
837 | * @param stdClass|string|null $next The next revision in list (that is |
||
838 | * the previous one in chronological order). |
||
839 | * May either be a row, "unknown" or null. |
||
840 | * @return string |
||
841 | */ |
||
842 | function lastLink( $prevRev, $next ) { |
||
843 | $last = $this->historyPage->message['last']; |
||
844 | |||
845 | if ( $next === null ) { |
||
846 | # Probably no next row |
||
847 | return $last; |
||
848 | } |
||
849 | |||
850 | if ( $next === 'unknown' ) { |
||
851 | # Next row probably exists but is unknown, use an oldid=prev link |
||
852 | return Linker::linkKnown( |
||
853 | $this->getTitle(), |
||
854 | $last, |
||
855 | [], |
||
856 | [ |
||
857 | 'diff' => $prevRev->getId(), |
||
858 | 'oldid' => 'prev' |
||
859 | ] |
||
860 | ); |
||
861 | } |
||
862 | |||
863 | $nextRev = new Revision( $next ); |
||
864 | |||
865 | if ( !$prevRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) |
||
866 | || !$nextRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) |
||
867 | ) { |
||
868 | return $last; |
||
869 | } |
||
870 | |||
871 | return Linker::linkKnown( |
||
872 | $this->getTitle(), |
||
873 | $last, |
||
874 | [], |
||
875 | [ |
||
876 | 'diff' => $prevRev->getId(), |
||
877 | 'oldid' => $next->rev_id |
||
878 | ] |
||
879 | ); |
||
880 | } |
||
881 | |||
882 | /** |
||
883 | * Create radio buttons for page history |
||
884 | * |
||
885 | * @param Revision $rev |
||
886 | * @param bool $firstInList Is this version the first one? |
||
887 | * |
||
888 | * @return string HTML output for the radio buttons |
||
889 | */ |
||
890 | function diffButtons( $rev, $firstInList ) { |
||
891 | if ( $this->getNumRows() > 1 ) { |
||
892 | $id = $rev->getId(); |
||
893 | $radio = [ 'type' => 'radio', 'value' => $id ]; |
||
894 | /** @todo Move title texts to javascript */ |
||
895 | if ( $firstInList ) { |
||
896 | $first = Xml::element( 'input', |
||
897 | array_merge( $radio, [ |
||
898 | 'style' => 'visibility:hidden', |
||
899 | 'name' => 'oldid', |
||
900 | 'id' => 'mw-oldid-null' ] ) |
||
901 | ); |
||
902 | $checkmark = [ 'checked' => 'checked' ]; |
||
903 | } else { |
||
904 | # Check visibility of old revisions |
||
905 | if ( !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { |
||
906 | $radio['disabled'] = 'disabled'; |
||
907 | $checkmark = []; // We will check the next possible one |
||
908 | } elseif ( !$this->oldIdChecked ) { |
||
909 | $checkmark = [ 'checked' => 'checked' ]; |
||
910 | $this->oldIdChecked = $id; |
||
911 | } else { |
||
912 | $checkmark = []; |
||
913 | } |
||
914 | $first = Xml::element( 'input', |
||
915 | array_merge( $radio, $checkmark, [ |
||
916 | 'name' => 'oldid', |
||
917 | 'id' => "mw-oldid-$id" ] ) ); |
||
918 | $checkmark = []; |
||
919 | } |
||
920 | $second = Xml::element( 'input', |
||
921 | array_merge( $radio, $checkmark, [ |
||
922 | 'name' => 'diff', |
||
923 | 'id' => "mw-diff-$id" ] ) ); |
||
924 | |||
925 | return $first . $second; |
||
926 | } else { |
||
927 | return ''; |
||
928 | } |
||
929 | } |
||
930 | |||
931 | /** |
||
932 | * This is called if a write operation is possible from the generated HTML |
||
933 | * @param bool $enable |
||
934 | */ |
||
935 | function preventClickjacking( $enable = true ) { |
||
936 | $this->preventClickjacking = $enable; |
||
937 | } |
||
938 | |||
939 | /** |
||
940 | * Get the "prevent clickjacking" flag |
||
941 | * @return bool |
||
942 | */ |
||
943 | function getPreventClickjacking() { |
||
944 | return $this->preventClickjacking; |
||
945 | } |
||
946 | |||
947 | } |
||
948 |
Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code: