MediaWiki  master
SpecialMergeHistory.php
Go to the documentation of this file.
1 <?php
24 namespace MediaWiki\Specials;
25 
26 use HTMLForm;
27 use LogEventsList;
28 use LogPage;
38 
47  protected $mAction;
48 
50  protected $mTarget;
51 
53  protected $mDest;
54 
56  protected $mTimestamp;
57 
59  protected $mTargetID;
60 
62  protected $mDestID;
63 
65  protected $mComment;
66 
68  protected $mMerge;
69 
71  protected $mSubmitted;
72 
74  protected $mTargetObj;
75 
77  protected $mDestObj;
78 
79  private MergeHistoryFactory $mergeHistoryFactory;
80  private LinkBatchFactory $linkBatchFactory;
81  private IConnectionProvider $dbProvider;
82  private RevisionStore $revisionStore;
83  private CommentFormatter $commentFormatter;
84 
86  private $mStatus;
87 
95  public function __construct(
96  MergeHistoryFactory $mergeHistoryFactory,
97  LinkBatchFactory $linkBatchFactory,
98  IConnectionProvider $dbProvider,
99  RevisionStore $revisionStore,
100  CommentFormatter $commentFormatter
101  ) {
102  parent::__construct( 'MergeHistory', 'mergehistory' );
103  $this->mergeHistoryFactory = $mergeHistoryFactory;
104  $this->linkBatchFactory = $linkBatchFactory;
105  $this->dbProvider = $dbProvider;
106  $this->revisionStore = $revisionStore;
107  $this->commentFormatter = $commentFormatter;
108  }
109 
110  public function doesWrites() {
111  return true;
112  }
113 
117  private function loadRequestParams() {
118  $request = $this->getRequest();
119  $this->mAction = $request->getRawVal( 'action' );
120  $this->mTarget = $request->getVal( 'target', '' );
121  $this->mDest = $request->getVal( 'dest', '' );
122  $this->mSubmitted = $request->getBool( 'submitted' );
123 
124  $this->mTargetID = intval( $request->getVal( 'targetID' ) );
125  $this->mDestID = intval( $request->getVal( 'destID' ) );
126  $this->mTimestamp = $request->getVal( 'mergepoint' );
127  if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
128  $this->mTimestamp = '';
129  }
130  $this->mComment = $request->getText( 'wpComment' );
131 
132  $this->mMerge = $request->wasPosted()
133  && $this->getContext()->getCsrfTokenSet()->matchToken( $request->getVal( 'wpEditToken' ) );
134 
135  // target page
136  if ( $this->mSubmitted ) {
137  $this->mTargetObj = Title::newFromText( $this->mTarget );
138  $this->mDestObj = Title::newFromText( $this->mDest );
139  } else {
140  $this->mTargetObj = null;
141  $this->mDestObj = null;
142  }
143  }
144 
145  public function execute( $par ) {
146  $this->useTransactionalTimeLimit();
147 
148  $this->checkPermissions();
149  $this->checkReadOnly();
150 
151  $this->loadRequestParams();
152 
153  $this->setHeaders();
154  $this->outputHeader();
155  $status = Status::newGood();
156 
157  if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
158  $this->merge();
159 
160  return;
161  }
162 
163  if ( !$this->mSubmitted ) {
164  $this->showMergeForm();
165 
166  return;
167  }
168 
169  if ( !$this->mTargetObj instanceof Title ) {
170  $status->merge( Status::newFatal( 'mergehistory-invalid-source' ) );
171  } elseif ( !$this->mTargetObj->exists() ) {
172  $status->merge( Status::newFatal(
173  'mergehistory-no-source',
174  wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
175  ) );
176  }
177 
178  if ( !$this->mDestObj instanceof Title ) {
179  $status->merge( Status::newFatal( 'mergehistory-invalid-destination' ) );
180  } elseif ( !$this->mDestObj->exists() ) {
181  $status->merge( Status::newFatal(
182  'mergehistory-no-destination',
183  wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
184  ) );
185  }
186 
187  if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
188  $status->merge( Status::newFatal( 'mergehistory-same-destination' ) );
189  }
190 
191  $this->mStatus = $status;
192 
193  $this->showMergeForm();
194 
195  if ( $this->mStatus->isGood() ) {
196  $this->showHistory();
197  }
198  }
199 
200  private function showMergeForm() {
201  $out = $this->getOutput();
202  $out->addWikiMsg( 'mergehistory-header' );
203 
204  $fields = [
205  'submitted' => [
206  'type' => 'hidden',
207  'default' => '1',
208  'name' => 'submitted'
209  ],
210  'title' => [
211  'type' => 'hidden',
212  'default' => $this->getPageTitle()->getPrefixedDBkey(),
213  'name' => 'title'
214  ],
215  'mergepoint' => [
216  'type' => 'hidden',
217  'default' => $this->mTimestamp,
218  'name' => 'mergepoint'
219  ],
220  'target' => [
221  'type' => 'title',
222  'label-message' => 'mergehistory-from',
223  'default' => $this->mTarget,
224  'id' => 'target',
225  'name' => 'target'
226  ],
227  'dest' => [
228  'type' => 'title',
229  'label-message' => 'mergehistory-into',
230  'default' => $this->mDest,
231  'id' => 'dest',
232  'name' => 'dest'
233  ]
234  ];
235 
236  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
237  $form->setWrapperLegendMsg( 'mergehistory-box' )
238  ->setSubmitTextMsg( 'mergehistory-go' )
239  ->setMethod( 'get' )
240  ->prepareForm()
241  ->displayForm( $this->mStatus );
242 
243  $this->addHelpLink( 'Help:Merge history' );
244  }
245 
246  private function showHistory() {
247  # List all stored revisions
248  $revisions = new MergeHistoryPager(
249  $this->getContext(),
250  $this->getLinkRenderer(),
251  $this->linkBatchFactory,
252  $this->dbProvider,
253  $this->revisionStore,
254  $this->commentFormatter,
255  [],
256  $this->mTargetObj,
257  $this->mDestObj,
258  $this->mTimestamp
259  );
260  $haveRevisions = $revisions->getNumRows() > 0;
261 
262  $out = $this->getOutput();
263  $out->addModuleStyles( [
264  'mediawiki.interface.helpers.styles',
265  'mediawiki.special'
266  ] );
267  $titleObj = $this->getPageTitle();
268  $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
269  # Start the form here
270  $fields = [
271  'targetID' => [
272  'type' => 'hidden',
273  'name' => 'targetID',
274  'default' => $this->mTargetObj->getArticleID()
275  ],
276  'destID' => [
277  'type' => 'hidden',
278  'name' => 'destID',
279  'default' => $this->mDestObj->getArticleID()
280  ],
281  'target' => [
282  'type' => 'hidden',
283  'name' => 'target',
284  'default' => $this->mTarget
285  ],
286  'dest' => [
287  'type' => 'hidden',
288  'name' => 'dest',
289  'default' => $this->mDest
290  ],
291  ];
292  if ( $haveRevisions ) {
293  $fields += [
294  'explanation' => [
295  'type' => 'info',
296  'default' => $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
297  $this->mDestObj->getPrefixedText() )->parse(),
298  'raw' => true,
299  'cssclass' => 'mw-mergehistory-explanation',
300  'section' => 'mergehistory-submit'
301  ],
302  'reason' => [
303  'type' => 'text',
304  'name' => 'wpComment',
305  'label-message' => 'mergehistory-reason',
306  'size' => 50,
307  'default' => $this->mComment,
308  'section' => 'mergehistory-submit'
309  ],
310  'submit' => [
311  'type' => 'submit',
312  'default' => $this->msg( 'mergehistory-submit' ),
313  'section' => 'mergehistory-submit',
314  'id' => 'mw-merge-submit',
315  'name' => 'merge'
316  ]
317  ];
318  }
319  $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
320  $form->addHiddenField( 'wpEditToken', $form->getCsrfTokenSet()->getToken() )
321  ->setId( 'merge' )
322  ->setAction( $action )
323  ->suppressDefaultSubmit();
324 
325  if ( $haveRevisions ) {
326  $form->setFooterHtml(
327  '<h2 id="mw-mergehistory">' . $this->msg( 'mergehistory-list' )->escaped() . '</h2>' .
328  $revisions->getNavigationBar() .
329  $revisions->getBody() .
330  $revisions->getNavigationBar()
331  );
332  } else {
333  $form->setFooterHtml( $this->msg( 'mergehistory-empty' ) );
334  }
335 
336  $form->prepareForm()->displayForm( false );
337 
338  # Show relevant lines from the merge log:
339  $mergeLogPage = new LogPage( 'merge' );
340  $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
341  LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
342 
343  return true;
344  }
345 
358  private function merge() {
359  # Get the titles directly from the IDs, in case the target page params
360  # were spoofed. The queries are done based on the IDs, so it's best to
361  # keep it consistent...
362  $targetTitle = Title::newFromID( $this->mTargetID );
363  $destTitle = Title::newFromID( $this->mDestID );
364  if ( $targetTitle === null || $destTitle === null ) {
365  return false; // validate these
366  }
367  if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
368  return false;
369  }
370 
371  // MergeHistory object
372  $mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
373 
374  // Merge!
375  $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
376  if ( !$mergeStatus->isOK() ) {
377  // Failed merge
378  $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
379  return false;
380  }
381 
382  $linkRenderer = $this->getLinkRenderer();
383 
384  $targetLink = $linkRenderer->makeLink(
385  $targetTitle,
386  null,
387  [],
388  [ 'redirect' => 'no' ]
389  );
390 
391  // In some cases the target page will be deleted
392  $append = ( $mergeStatus->getValue() === 'source-deleted' )
393  ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
394 
395  $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
396  ->rawParams( $targetLink )
397  ->params( $destTitle->getPrefixedText(), $append )
398  ->numParams( $mh->getMergedRevisionCount() )
399  );
400 
401  return true;
402  }
403 
404  protected function getGroupName() {
405  return 'pagetools';
406  }
407 }
408 
413 class_alias( SpecialMergeHistory::class, 'SpecialMergeHistory' );
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:158
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:360
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:43
This is the main service interface for converting single-line comments from various DB comment fields...
Service for looking up page revisions.
Parent class for all special pages.
Definition: SpecialPage.php:66
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
getPageTitle( $subpage=false)
Get a self-referential title object.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Special page allowing users with the appropriate permissions to merge article histories,...
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
doesWrites()
Indicates whether this special page may perform database writes.
execute( $par)
Default execute method Checks user permissions.
__construct(MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, RevisionStore $revisionStore, CommentFormatter $commentFormatter)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
Represents a title within MediaWiki.
Definition: Title.php:76
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:534
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:400
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Service for mergehistory actions.
Provide primary and replica IDatabase connections.