MediaWiki REL1_39
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
29
38 protected $mAction;
39
41 protected $mTarget;
42
44 protected $mDest;
45
47 protected $mTimestamp;
48
50 protected $mTargetID;
51
53 protected $mDestID;
54
56 protected $mComment;
57
59 protected $mMerge;
60
62 protected $mSubmitted;
63
65 protected $mTargetObj;
66
68 protected $mDestObj;
69
71 public $prevId;
72
74 private $mergeHistoryFactory;
75
77 private $linkBatchFactory;
78
80 private $loadBalancer;
81
83 private $revisionStore;
84
91 public function __construct(
92 MergeHistoryFactory $mergeHistoryFactory,
93 LinkBatchFactory $linkBatchFactory,
94 ILoadBalancer $loadBalancer,
95 RevisionStore $revisionStore
96 ) {
97 parent::__construct( 'MergeHistory', 'mergehistory' );
98 $this->mergeHistoryFactory = $mergeHistoryFactory;
99 $this->linkBatchFactory = $linkBatchFactory;
100 $this->loadBalancer = $loadBalancer;
101 $this->revisionStore = $revisionStore;
102 }
103
104 public function doesWrites() {
105 return true;
106 }
107
111 private function loadRequestParams() {
112 $request = $this->getRequest();
113 $this->mAction = $request->getRawVal( 'action' );
114 $this->mTarget = $request->getVal( 'target', '' );
115 $this->mDest = $request->getVal( 'dest', '' );
116 $this->mSubmitted = $request->getBool( 'submitted' );
117
118 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
119 $this->mDestID = intval( $request->getVal( 'destID' ) );
120 $this->mTimestamp = $request->getVal( 'mergepoint' );
121 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
122 $this->mTimestamp = '';
123 }
124 $this->mComment = $request->getText( 'wpComment' );
125
126 $this->mMerge = $request->wasPosted()
127 && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
128
129 // target page
130 if ( $this->mSubmitted ) {
131 $this->mTargetObj = Title::newFromText( $this->mTarget );
132 $this->mDestObj = Title::newFromText( $this->mDest );
133 } else {
134 $this->mTargetObj = null;
135 $this->mDestObj = null;
136 }
137 }
138
139 public function execute( $par ) {
141
142 $this->checkPermissions();
143 $this->checkReadOnly();
144
145 $this->loadRequestParams();
146
147 $this->setHeaders();
148 $this->outputHeader();
149
150 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
151 $this->merge();
152
153 return;
154 }
155
156 if ( !$this->mSubmitted ) {
157 $this->showMergeForm();
158
159 return;
160 }
161
162 $errors = [];
163 if ( !$this->mTargetObj instanceof Title ) {
164 $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
165 } elseif ( !$this->mTargetObj->exists() ) {
166 $errors[] = $this->msg( 'mergehistory-no-source',
167 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
168 )->parseAsBlock();
169 }
170
171 if ( !$this->mDestObj instanceof Title ) {
172 $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
173 } elseif ( !$this->mDestObj->exists() ) {
174 $errors[] = $this->msg( 'mergehistory-no-destination',
175 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
176 )->parseAsBlock();
177 }
178
179 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
180 $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
181 }
182
183 if ( count( $errors ) ) {
184 $this->showMergeForm();
185 $this->getOutput()->addHTML( implode( "\n", $errors ) );
186 } else {
187 $this->showHistory();
188 }
189 }
190
191 private function showMergeForm() {
192 $out = $this->getOutput();
193 $out->addWikiMsg( 'mergehistory-header' );
194
195 $out->addHTML(
196 Xml::openElement( 'form', [
197 'method' => 'get',
198 'action' => wfScript() ] ) .
199 '<fieldset>' .
200 Xml::element( 'legend', [],
201 $this->msg( 'mergehistory-box' )->text() ) .
202 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
203 Html::hidden( 'submitted', '1' ) .
204 Html::hidden( 'mergepoint', $this->mTimestamp ) .
205 Xml::openElement( 'table' ) .
206 '<tr>
207 <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
208 <td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td>
209 </tr><tr>
210 <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
211 <td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td>
212 </tr><tr><td>' .
213 Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
214 '</td></tr>' .
215 Xml::closeElement( 'table' ) .
216 '</fieldset>' .
217 '</form>'
218 );
219
220 $this->addHelpLink( 'Help:Merge history' );
221 }
222
223 private function showHistory() {
224 $this->showMergeForm();
225
226 # List all stored revisions
227 $revisions = new MergeHistoryPager(
228 $this,
229 $this->linkBatchFactory,
230 $this->loadBalancer,
231 $this->revisionStore,
232 [],
233 $this->mTargetObj,
234 $this->mDestObj
235 );
236 $haveRevisions = $revisions->getNumRows() > 0;
237
238 $out = $this->getOutput();
239 $out->addModuleStyles( [ 'mediawiki.interface.helpers.styles' ] );
240 $titleObj = $this->getPageTitle();
241 $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
242 # Start the form here
243 $top = Xml::openElement(
244 'form',
245 [
246 'method' => 'post',
247 'action' => $action,
248 'id' => 'merge'
249 ]
250 );
251 $out->addHTML( $top );
252
253 if ( $haveRevisions ) {
254 # Format the user-visible controls (comment field, submission button)
255 # in a nice little table
256 $table =
257 Xml::openElement( 'fieldset' ) .
258 $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
259 $this->mDestObj->getPrefixedText() )->parse() .
260 Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) .
261 '<tr>
262 <td class="mw-label">' .
263 Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
264 '</td>
265 <td class="mw-input">' .
266 Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) .
267 "</td>
268 </tr>
269 <tr>
270 <td>\u{00A0}</td>
271 <td class=\"mw-submit\">" .
273 $this->msg( 'mergehistory-submit' )->text(),
274 [ 'name' => 'merge', 'id' => 'mw-merge-submit' ]
275 ) .
276 '</td>
277 </tr>' .
278 Xml::closeElement( 'table' ) .
279 Xml::closeElement( 'fieldset' );
280
281 $out->addHTML( $table );
282 }
283
284 $out->addHTML(
285 '<h2 id="mw-mergehistory">' .
286 $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
287 );
288
289 if ( $haveRevisions ) {
290 $out->addHTML( $revisions->getNavigationBar() );
291 $out->addHTML( $revisions->getBody() );
292 $out->addHTML( $revisions->getNavigationBar() );
293 } else {
294 $out->addWikiMsg( 'mergehistory-empty' );
295 }
296
297 # Show relevant lines from the merge log:
298 $mergeLogPage = new LogPage( 'merge' );
299 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
300 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
301
302 # When we submit, go by page ID to avoid some nasty but unlikely collisions.
303 # Such would happen if a page was renamed after the form loaded, but before submit
304 $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
305 $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
306 $misc .= Html::hidden( 'target', $this->mTarget );
307 $misc .= Html::hidden( 'dest', $this->mDest );
308 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
309 $misc .= Xml::closeElement( 'form' );
310 $out->addHTML( $misc );
311
312 return true;
313 }
314
315 public function formatRevisionRow( $row ) {
316 $revRecord = $this->revisionStore->newRevisionFromRow( $row );
317
318 $linkRenderer = $this->getLinkRenderer();
319
320 $stxt = '';
321 $last = $this->msg( 'last' )->escaped();
322
323 $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
324 $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
325
326 $user = $this->getUser();
327
328 $pageLink = $linkRenderer->makeKnownLink(
329 $revRecord->getPageAsLinkTarget(),
330 $this->getLanguage()->userTimeAndDate( $ts, $user ),
331 [],
332 [ 'oldid' => $revRecord->getId() ]
333 );
334 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
335 $class = Linker::getRevisionDeletedClass( $revRecord );
336 $pageLink = '<span class=" ' . $class . '">' . $pageLink . '</span>';
337 }
338
339 # Last link
340 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
341 $last = $this->msg( 'last' )->escaped();
342 } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
343 $last = $linkRenderer->makeKnownLink(
344 $revRecord->getPageAsLinkTarget(),
345 $this->msg( 'last' )->text(),
346 [],
347 [
348 'diff' => $row->rev_id,
349 'oldid' => $this->prevId[$row->rev_id]
350 ]
351 );
352 }
353
354 $userLink = Linker::revUserTools( $revRecord );
355
356 $size = $row->rev_len;
357 if ( $size !== null ) {
358 $stxt = Linker::formatRevisionSize( $size );
359 }
360 $comment = Linker::revComment( $revRecord );
361
362 // Tags, if any.
363 list( $tagSummary, $classes ) = ChangeTags::formatSummaryRow(
364 $row->ts_tags,
365 'mergehistory',
366 $this->getContext()
367 );
368
369 return Html::rawElement( 'li', $classes,
370 $this->msg( 'mergehistory-revisionrow' )
371 ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment, $tagSummary )->escaped() );
372 }
373
386 private function merge() {
387 # Get the titles directly from the IDs, in case the target page params
388 # were spoofed. The queries are done based on the IDs, so it's best to
389 # keep it consistent...
390 $targetTitle = Title::newFromID( $this->mTargetID );
391 $destTitle = Title::newFromID( $this->mDestID );
392 if ( $targetTitle === null || $destTitle === null ) {
393 return false; // validate these
394 }
395 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
396 return false;
397 }
398
399 // MergeHistory object
400 $mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
401
402 // Merge!
403 $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
404 if ( !$mergeStatus->isOK() ) {
405 // Failed merge
406 $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
407 return false;
408 }
409
410 $linkRenderer = $this->getLinkRenderer();
411
412 $targetLink = $linkRenderer->makeLink(
413 $targetTitle,
414 null,
415 [],
416 [ 'redirect' => 'no' ]
417 );
418
419 // In some cases the target page will be deleted
420 $append = ( $mergeStatus->getValue() === 'source-deleted' )
421 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
422
423 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
424 ->rawParams( $targetLink )
425 ->params( $destTitle->getPrefixedText(), $append )
426 ->numParams( $mh->getMergedRevisionCount() )
427 );
428
429 return true;
430 }
431
432 protected function getGroupName() {
433 return 'pagetools';
434 }
435}
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, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition Html.php:851
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1351
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1607
static formatRevisionSize( $size)
Definition Linker.php:1623
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1371
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:39
Page revision base class.
Service for looking up page revisions.
Special page allowing users with the appropriate permissions to merge article histories,...
__construct(MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, ILoadBalancer $loadBalancer, RevisionStore $revisionStore)
doesWrites()
Indicates whether this special page may perform database writes.
bool $mSubmitted
Was submitted?
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.
Represents a title within MediaWiki.
Definition Title.php:49
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:370
static closeElement( $element)
Shortcut to close an XML element.
Definition Xml.php:121
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition Xml.php:367
static openElement( $element, $attribs=null)
This opens an XML element.
Definition Xml.php:112
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition Xml.php:283
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition Xml.php:469
Service for mergehistory actions.
Create and track the database connections and transactions for a given database cluster.