MediaWiki  master
SpecialMergeHistory.php
Go to the documentation of this file.
1 <?php
25 
34  protected $mAction;
35 
37  protected $mTarget;
38 
40  protected $mDest;
41 
43  protected $mTimestamp;
44 
46  protected $mTargetID;
47 
49  protected $mDestID;
50 
52  protected $mComment;
53 
55  protected $mMerge;
56 
58  protected $mSubmitted;
59 
61  protected $mTargetObj;
62 
64  protected $mDestObj;
65 
67  public $prevId;
68 
69  public function __construct() {
70  parent::__construct( 'MergeHistory', 'mergehistory' );
71  }
72 
73  public function doesWrites() {
74  return true;
75  }
76 
80  private function loadRequestParams() {
81  $request = $this->getRequest();
82  $this->mAction = $request->getVal( 'action' );
83  $this->mTarget = $request->getVal( 'target' );
84  $this->mDest = $request->getVal( 'dest' );
85  $this->mSubmitted = $request->getBool( 'submitted' );
86 
87  $this->mTargetID = intval( $request->getVal( 'targetID' ) );
88  $this->mDestID = intval( $request->getVal( 'destID' ) );
89  $this->mTimestamp = $request->getVal( 'mergepoint' );
90  if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
91  $this->mTimestamp = '';
92  }
93  $this->mComment = $request->getText( 'wpComment' );
94 
95  $this->mMerge = $request->wasPosted()
96  && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
97 
98  // target page
99  if ( $this->mSubmitted ) {
100  $this->mTargetObj = Title::newFromText( $this->mTarget );
101  $this->mDestObj = Title::newFromText( $this->mDest );
102  } else {
103  $this->mTargetObj = null;
104  $this->mDestObj = null;
105  }
106  }
107 
108  public function execute( $par ) {
109  $this->useTransactionalTimeLimit();
110 
111  $this->checkPermissions();
112  $this->checkReadOnly();
113 
114  $this->loadRequestParams();
115 
116  $this->setHeaders();
117  $this->outputHeader();
118 
119  if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
120  $this->merge();
121 
122  return;
123  }
124 
125  if ( !$this->mSubmitted ) {
126  $this->showMergeForm();
127 
128  return;
129  }
130 
131  $errors = [];
132  if ( !$this->mTargetObj instanceof Title ) {
133  $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
134  } elseif ( !$this->mTargetObj->exists() ) {
135  $errors[] = $this->msg( 'mergehistory-no-source',
136  wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
137  )->parseAsBlock();
138  }
139 
140  if ( !$this->mDestObj instanceof Title ) {
141  $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
142  } elseif ( !$this->mDestObj->exists() ) {
143  $errors[] = $this->msg( 'mergehistory-no-destination',
144  wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
145  )->parseAsBlock();
146  }
147 
148  if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
149  $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
150  }
151 
152  if ( count( $errors ) ) {
153  $this->showMergeForm();
154  $this->getOutput()->addHTML( implode( "\n", $errors ) );
155  } else {
156  $this->showHistory();
157  }
158  }
159 
160  function showMergeForm() {
161  $out = $this->getOutput();
162  $out->addWikiMsg( 'mergehistory-header' );
163 
164  $out->addHTML(
165  Xml::openElement( 'form', [
166  'method' => 'get',
167  'action' => wfScript() ] ) .
168  '<fieldset>' .
169  Xml::element( 'legend', [],
170  $this->msg( 'mergehistory-box' )->text() ) .
171  Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
172  Html::hidden( 'submitted', '1' ) .
173  Html::hidden( 'mergepoint', $this->mTimestamp ) .
174  Xml::openElement( 'table' ) .
175  '<tr>
176  <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
177  <td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td>
178  </tr><tr>
179  <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
180  <td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td>
181  </tr><tr><td>' .
182  Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
183  '</td></tr>' .
184  Xml::closeElement( 'table' ) .
185  '</fieldset>' .
186  '</form>'
187  );
188 
189  $this->addHelpLink( 'Help:Merge history' );
190  }
191 
192  private function showHistory() {
193  $this->showMergeForm();
194 
195  # List all stored revisions
196  $revisions = new MergeHistoryPager(
197  $this, [], $this->mTargetObj, $this->mDestObj
198  );
199  $haveRevisions = $revisions && $revisions->getNumRows() > 0;
200 
201  $out = $this->getOutput();
202  $titleObj = $this->getPageTitle();
203  $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
204  # Start the form here
205  $top = Xml::openElement(
206  'form',
207  [
208  'method' => 'post',
209  'action' => $action,
210  'id' => 'merge'
211  ]
212  );
213  $out->addHTML( $top );
214 
215  if ( $haveRevisions ) {
216  # Format the user-visible controls (comment field, submission button)
217  # in a nice little table
218  $table =
219  Xml::openElement( 'fieldset' ) .
220  $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
221  $this->mDestObj->getPrefixedText() )->parse() .
222  Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) .
223  '<tr>
224  <td class="mw-label">' .
225  Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
226  '</td>
227  <td class="mw-input">' .
228  Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) .
229  "</td>
230  </tr>
231  <tr>
232  <td>\u{00A0}</td>
233  <td class=\"mw-submit\">" .
235  $this->msg( 'mergehistory-submit' )->text(),
236  [ 'name' => 'merge', 'id' => 'mw-merge-submit' ]
237  ) .
238  '</td>
239  </tr>' .
240  Xml::closeElement( 'table' ) .
241  Xml::closeElement( 'fieldset' );
242 
243  $out->addHTML( $table );
244  }
245 
246  $out->addHTML(
247  '<h2 id="mw-mergehistory">' .
248  $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
249  );
250 
251  if ( $haveRevisions ) {
252  $out->addHTML( $revisions->getNavigationBar() );
253  $out->addHTML( '<ul>' );
254  $out->addHTML( $revisions->getBody() );
255  $out->addHTML( '</ul>' );
256  $out->addHTML( $revisions->getNavigationBar() );
257  } else {
258  $out->addWikiMsg( 'mergehistory-empty' );
259  }
260 
261  # Show relevant lines from the merge log:
262  $mergeLogPage = new LogPage( 'merge' );
263  $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
264  LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
265 
266  # When we submit, go by page ID to avoid some nasty but unlikely collisions.
267  # Such would happen if a page was renamed after the form loaded, but before submit
268  $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
269  $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
270  $misc .= Html::hidden( 'target', $this->mTarget );
271  $misc .= Html::hidden( 'dest', $this->mDest );
272  $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
273  $misc .= Xml::closeElement( 'form' );
274  $out->addHTML( $misc );
275 
276  return true;
277  }
278 
279  function formatRevisionRow( $row ) {
280  $rev = new Revision( $row );
281 
282  $linkRenderer = $this->getLinkRenderer();
283 
284  $stxt = '';
285  $last = $this->msg( 'last' )->escaped();
286 
287  $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
288  $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
289 
290  $user = $this->getUser();
291 
292  $pageLink = $linkRenderer->makeKnownLink(
293  $rev->getTitle(),
294  $this->getLanguage()->userTimeAndDate( $ts, $user ),
295  [],
296  [ 'oldid' => $rev->getId() ]
297  );
298  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
299  $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
300  }
301 
302  # Last link
303  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
304  $last = $this->msg( 'last' )->escaped();
305  } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
306  $last = $linkRenderer->makeKnownLink(
307  $rev->getTitle(),
308  $this->msg( 'last' )->text(),
309  [],
310  [
311  'diff' => $row->rev_id,
312  'oldid' => $this->prevId[$row->rev_id]
313  ]
314  );
315  }
316 
317  $userLink = Linker::revUserTools( $rev );
318 
319  $size = $row->rev_len;
320  if ( !is_null( $size ) ) {
321  $stxt = Linker::formatRevisionSize( $size );
322  }
323  $comment = Linker::revComment( $rev );
324 
325  return Html::rawElement( 'li', [],
326  $this->msg( 'mergehistory-revisionrow' )
327  ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
328  }
329 
342  function merge() {
343  # Get the titles directly from the IDs, in case the target page params
344  # were spoofed. The queries are done based on the IDs, so it's best to
345  # keep it consistent...
346  $targetTitle = Title::newFromID( $this->mTargetID );
347  $destTitle = Title::newFromID( $this->mDestID );
348  if ( is_null( $targetTitle ) || is_null( $destTitle ) ) {
349  return false; // validate these
350  }
351  if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
352  return false;
353  }
354 
355  // MergeHistory object
356  $mh = new MergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
357 
358  // Merge!
359  $mergeStatus = $mh->merge( $this->getUser(), $this->mComment );
360  if ( !$mergeStatus->isOK() ) {
361  // Failed merge
362  $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
363  return false;
364  }
365 
366  $linkRenderer = $this->getLinkRenderer();
367 
368  $targetLink = $linkRenderer->makeLink(
369  $targetTitle,
370  null,
371  [],
372  [ 'redirect' => 'no' ]
373  );
374 
375  $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
376  ->rawParams( $targetLink )
377  ->params( $destTitle->getPrefixedText() )
378  ->numParams( $mh->getMergedRevisionCount() )
379  );
380 
381  return true;
382  }
383 
384  protected function getGroupName() {
385  return 'pagetools';
386  }
387 }
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:467
static formatRevisionSize( $size)
Definition: Linker.php:1597
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:274
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
bool $mMerge
Was posted?
getOutput()
Get the OutputPage being used for this instance.
Class to simplify the use of log pages.
Definition: LogPage.php:33
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
$last
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:459
Special page allowing users with the appropriate permissions to merge article histories, with some restrictions.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes! ...
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition: Xml.php:358
bool $mSubmitted
Was submitted?
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getUser()
Shortcut to get the User executing this instance.
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getLanguage()
Shortcut to get user&#39;s language.
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision&#39;s comment block, if the current user is allowed to view it...
Definition: Linker.php:1572
Handles the backend logic of merging the histories of two pages.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
merge()
Actually attempt the history move.
getRequest()
Get the WebRequest being used for this instance.
static radio( $name, $value, $checked=false, $attribs=[])
Convenience function to build an HTML radio button.
Definition: Xml.php:341
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getPageTitle( $subpage=false)
Get a self-referential title object.
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1124
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319