MediaWiki 1.40.4
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
33
42 protected $mAction;
43
45 protected $mTarget;
46
48 protected $mDest;
49
51 protected $mTimestamp;
52
54 protected $mTargetID;
55
57 protected $mDestID;
58
60 protected $mComment;
61
63 protected $mMerge;
64
66 protected $mSubmitted;
67
69 protected $mTargetObj;
70
72 protected $mDestObj;
73
75 public $prevId;
76
78 private $mergeHistoryFactory;
79
81 private $linkBatchFactory;
82
84 private $loadBalancer;
85
87 private $revisionStore;
88
90 private $commentFormatter;
91
99 public function __construct(
100 MergeHistoryFactory $mergeHistoryFactory,
101 LinkBatchFactory $linkBatchFactory,
102 ILoadBalancer $loadBalancer,
103 RevisionStore $revisionStore,
104 CommentFormatter $commentFormatter
105 ) {
106 parent::__construct( 'MergeHistory', 'mergehistory' );
107 $this->mergeHistoryFactory = $mergeHistoryFactory;
108 $this->linkBatchFactory = $linkBatchFactory;
109 $this->loadBalancer = $loadBalancer;
110 $this->revisionStore = $revisionStore;
111 $this->commentFormatter = $commentFormatter;
112 }
113
114 public function doesWrites() {
115 return true;
116 }
117
121 private function loadRequestParams() {
122 $request = $this->getRequest();
123 $this->mAction = $request->getRawVal( 'action' );
124 $this->mTarget = $request->getVal( 'target', '' );
125 $this->mDest = $request->getVal( 'dest', '' );
126 $this->mSubmitted = $request->getBool( 'submitted' );
127
128 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
129 $this->mDestID = intval( $request->getVal( 'destID' ) );
130 $this->mTimestamp = $request->getVal( 'mergepoint' );
131 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
132 $this->mTimestamp = '';
133 }
134 $this->mComment = $request->getText( 'wpComment' );
135
136 $this->mMerge = $request->wasPosted()
137 && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
138
139 // target page
140 if ( $this->mSubmitted ) {
141 $this->mTargetObj = Title::newFromText( $this->mTarget );
142 $this->mDestObj = Title::newFromText( $this->mDest );
143 } else {
144 $this->mTargetObj = null;
145 $this->mDestObj = null;
146 }
147 }
148
149 public function execute( $par ) {
151
152 $this->checkPermissions();
153 $this->checkReadOnly();
154
155 $this->loadRequestParams();
156
157 $this->setHeaders();
158 $this->outputHeader();
159
160 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
161 $this->merge();
162
163 return;
164 }
165
166 if ( !$this->mSubmitted ) {
167 $this->showMergeForm();
168
169 return;
170 }
171
172 $errors = [];
173 if ( !$this->mTargetObj instanceof Title ) {
174 $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
175 } elseif ( !$this->mTargetObj->exists() ) {
176 $errors[] = $this->msg( 'mergehistory-no-source',
177 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
178 )->parseAsBlock();
179 }
180
181 if ( !$this->mDestObj instanceof Title ) {
182 $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
183 } elseif ( !$this->mDestObj->exists() ) {
184 $errors[] = $this->msg( 'mergehistory-no-destination',
185 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
186 )->parseAsBlock();
187 }
188
189 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
190 $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
191 }
192
193 if ( count( $errors ) ) {
194 $this->showMergeForm();
195 $this->getOutput()->addHTML( implode( "\n", $errors ) );
196 } else {
197 $this->showHistory();
198 }
199 }
200
201 private function showMergeForm() {
202 $out = $this->getOutput();
203 $out->addWikiMsg( 'mergehistory-header' );
204
205 $out->addHTML(
206 Xml::openElement( 'form', [
207 'method' => 'get',
208 'action' => wfScript() ] ) .
209 '<fieldset>' .
210 Xml::element( 'legend', [],
211 $this->msg( 'mergehistory-box' )->text() ) .
212 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
213 Html::hidden( 'submitted', '1' ) .
214 Html::hidden( 'mergepoint', $this->mTimestamp ) .
215 Xml::openElement( 'table' ) .
216 '<tr>
217 <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
218 <td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td>
219 </tr><tr>
220 <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
221 <td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td>
222 </tr><tr><td>' .
223 Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
224 '</td></tr>' .
225 Xml::closeElement( 'table' ) .
226 '</fieldset>' .
227 '</form>'
228 );
229
230 $this->addHelpLink( 'Help:Merge history' );
231 }
232
233 private function showHistory() {
234 $this->showMergeForm();
235
236 # List all stored revisions
237 $revisions = new MergeHistoryPager(
238 $this,
239 $this->linkBatchFactory,
240 $this->loadBalancer,
241 $this->revisionStore,
242 [],
243 $this->mTargetObj,
244 $this->mDestObj
245 );
246 $haveRevisions = $revisions->getNumRows() > 0;
247
248 $out = $this->getOutput();
249 $out->addModuleStyles( [ 'mediawiki.interface.helpers.styles' ] );
250 $titleObj = $this->getPageTitle();
251 $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
252 # Start the form here
253 $top = Xml::openElement(
254 'form',
255 [
256 'method' => 'post',
257 'action' => $action,
258 'id' => 'merge'
259 ]
260 );
261 $out->addHTML( $top );
262
263 if ( $haveRevisions ) {
264 # Format the user-visible controls (comment field, submission button)
265 # in a nice little table
266 $table =
267 Xml::openElement( 'fieldset' ) .
268 $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
269 $this->mDestObj->getPrefixedText() )->parse() .
270 Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) .
271 '<tr>
272 <td class="mw-label">' .
273 Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
274 '</td>
275 <td class="mw-input">' .
276 Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) .
277 "</td>
278 </tr>
279 <tr>
280 <td>\u{00A0}</td>
281 <td class=\"mw-submit\">" .
283 $this->msg( 'mergehistory-submit' )->text(),
284 [ 'name' => 'merge', 'id' => 'mw-merge-submit' ]
285 ) .
286 '</td>
287 </tr>' .
288 Xml::closeElement( 'table' ) .
289 Xml::closeElement( 'fieldset' );
290
291 $out->addHTML( $table );
292 }
293
294 $out->addHTML(
295 '<h2 id="mw-mergehistory">' .
296 $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
297 );
298
299 if ( $haveRevisions ) {
300 $out->addHTML( $revisions->getNavigationBar() );
301 $out->addHTML( $revisions->getBody() );
302 $out->addHTML( $revisions->getNavigationBar() );
303 } else {
304 $out->addWikiMsg( 'mergehistory-empty' );
305 }
306
307 # Show relevant lines from the merge log:
308 $mergeLogPage = new LogPage( 'merge' );
309 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
310 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
311
312 # When we submit, go by page ID to avoid some nasty but unlikely collisions.
313 # Such would happen if a page was renamed after the form loaded, but before submit
314 $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
315 $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
316 $misc .= Html::hidden( 'target', $this->mTarget );
317 $misc .= Html::hidden( 'dest', $this->mDest );
318 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
319 $misc .= Xml::closeElement( 'form' );
320 $out->addHTML( $misc );
321
322 return true;
323 }
324
325 public function formatRevisionRow( $row ) {
326 $revRecord = $this->revisionStore->newRevisionFromRow( $row );
327
328 $linkRenderer = $this->getLinkRenderer();
329
330 $stxt = '';
331 $last = $this->msg( 'last' )->escaped();
332
333 $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
334 $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
335
336 $user = $this->getUser();
337
338 $pageLink = $linkRenderer->makeKnownLink(
339 $revRecord->getPageAsLinkTarget(),
340 $this->getLanguage()->userTimeAndDate( $ts, $user ),
341 [],
342 [ 'oldid' => $revRecord->getId() ]
343 );
344 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
345 $class = Linker::getRevisionDeletedClass( $revRecord );
346 $pageLink = '<span class=" ' . $class . '">' . $pageLink . '</span>';
347 }
348
349 # Last link
350 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
351 $last = $this->msg( 'last' )->escaped();
352 } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
353 $last = $linkRenderer->makeKnownLink(
354 $revRecord->getPageAsLinkTarget(),
355 $this->msg( 'last' )->text(),
356 [],
357 [
358 'diff' => $row->rev_id,
359 'oldid' => $this->prevId[$row->rev_id]
360 ]
361 );
362 }
363
364 $userLink = Linker::revUserTools( $revRecord );
365
366 $size = $row->rev_len;
367 if ( $size !== null ) {
368 $stxt = Linker::formatRevisionSize( $size );
369 }
370 $comment = $this->commentFormatter->formatRevision( $revRecord, $user );
371
372 // Tags, if any.
373 [ $tagSummary, $classes ] = ChangeTags::formatSummaryRow(
374 $row->ts_tags,
375 'mergehistory',
376 $this->getContext()
377 );
378
379 return Html::rawElement( 'li', $classes,
380 $this->msg( 'mergehistory-revisionrow' )
381 ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment, $tagSummary )->escaped() );
382 }
383
396 private function merge() {
397 # Get the titles directly from the IDs, in case the target page params
398 # were spoofed. The queries are done based on the IDs, so it's best to
399 # keep it consistent...
400 $targetTitle = Title::newFromID( $this->mTargetID );
401 $destTitle = Title::newFromID( $this->mDestID );
402 if ( $targetTitle === null || $destTitle === null ) {
403 return false; // validate these
404 }
405 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
406 return false;
407 }
408
409 // MergeHistory object
410 $mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
411
412 // Merge!
413 $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
414 if ( !$mergeStatus->isOK() ) {
415 // Failed merge
416 $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
417 return false;
418 }
419
420 $linkRenderer = $this->getLinkRenderer();
421
422 $targetLink = $linkRenderer->makeLink(
423 $targetTitle,
424 null,
425 [],
426 [ 'redirect' => 'no' ]
427 );
428
429 // In some cases the target page will be deleted
430 $append = ( $mergeStatus->getValue() === 'source-deleted' )
431 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
432
433 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
434 ->rawParams( $targetLink )
435 ->params( $destTitle->getPrefixedText(), $append )
436 ->numParams( $mh->getMergedRevisionCount() )
437 );
438
439 return true;
440 }
441
442 protected function getGroupName() {
443 return 'pagetools';
444 }
445}
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:41
This is the main service interface for converting single-line comments from various DB comment fields...
This class is a collection of static functions that serve two purposes:
Definition Html.php:55
Some internal bits split of from Skin.php.
Definition Linker.php:67
Page revision base class.
Service for looking up page revisions.
Represents a title within MediaWiki.
Definition Title.php:82
Special page allowing users with the appropriate permissions to merge article histories,...
doesWrites()
Indicates whether this special page may perform database writes.
bool $mSubmitted
Was submitted?
__construct(MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, ILoadBalancer $loadBalancer, RevisionStore $revisionStore, CommentFormatter $commentFormatter)
execute( $par)
Default execute method Checks user permissions.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getAuthority()
Shortcut to get the Authority executing this instance.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:122
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition Xml.php:365
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:113
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition Xml.php:281
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition Xml.php:467
Service for mergehistory actions.
This class is a delegate to ILBFactory for a given database cluster.