MediaWiki  master
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 ) {
150  $this->useTransactionalTimeLimit();
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.
Definition: ChangeTags.php:198
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:65
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.
Definition: SpecialPage.php:45
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
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:44
static radio( $name, $value, $checked=false, $attribs=[])
Convenience function to build an HTML radio button.
Definition: Xml.php:348
Service for mergehistory actions.
This class is a delegate to ILBFactory for a given database cluster.