MediaWiki  master
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 ) {
140  $this->useTransactionalTimeLimit();
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.
Definition: ChangeTags.php:193
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
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:1307
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:1563
static formatRevisionSize( $size)
Definition: Linker.php:1579
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:1327
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.
Definition: SpecialPage.php:44
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 newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:518
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
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:43
static radio( $name, $value, $checked=false, $attribs=[])
Convenience function to build an HTML radio button.
Definition: Xml.php:350
Service for mergehistory actions.
Create and track the database connections and transactions for a given database cluster.