MediaWiki master
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
9use MediaWiki\Cache\LinkBatchFactory;
16use MediaWiki\Pager\MergeHistoryPager;
22
34 protected $mAction;
35
37 protected $mTarget;
38
40 protected $mDest;
41
43 protected $mTimestamp;
44
46 protected $mTimestampOld;
47
49 protected $mTargetID;
50
52 protected $mDestID;
53
55 protected $mComment;
56
58 protected $mMerge;
59
61 protected $mSubmitted;
62
64 protected $mTargetObj;
65
67 protected $mDestObj;
68
69 private MergeHistoryFactory $mergeHistoryFactory;
70 private LinkBatchFactory $linkBatchFactory;
71 private IConnectionProvider $dbProvider;
72 private RevisionStore $revisionStore;
73 private CommentFormatter $commentFormatter;
74 private ChangeTagsStore $changeTagsStore;
75
77 private $mStatus;
78
79 public function __construct(
80 MergeHistoryFactory $mergeHistoryFactory,
81 LinkBatchFactory $linkBatchFactory,
82 IConnectionProvider $dbProvider,
83 RevisionStore $revisionStore,
84 CommentFormatter $commentFormatter,
85 ChangeTagsStore $changeTagsStore
86 ) {
87 parent::__construct( 'MergeHistory', 'mergehistory' );
88 $this->mergeHistoryFactory = $mergeHistoryFactory;
89 $this->linkBatchFactory = $linkBatchFactory;
90 $this->dbProvider = $dbProvider;
91 $this->revisionStore = $revisionStore;
92 $this->commentFormatter = $commentFormatter;
93 $this->changeTagsStore = $changeTagsStore;
94 }
95
97 public function doesWrites() {
98 return true;
99 }
100
104 private function loadRequestParams() {
105 $request = $this->getRequest();
106 $this->mAction = $request->getRawVal( 'action' );
107 $this->mTarget = $request->getVal( 'target', '' );
108 $this->mDest = $request->getVal( 'dest', '' );
109 $this->mSubmitted = $request->getBool( 'submitted' );
110
111 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
112 $this->mDestID = intval( $request->getVal( 'destID' ) );
113 $this->mTimestamp = $request->getVal( 'mergepoint' );
114 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}(\|[0-9]+)?/', $this->mTimestamp ) ) {
115 $this->mTimestamp = '';
116 }
117 $this->mTimestampOld = $request->getVal( 'mergepointold' );
118 if ( $this->mTimestampOld === null || !preg_match( '/[0-9]{14}(\|[0-9]+)?/', $this->mTimestamp ) ) {
119 $this->mTimestampOld = '';
120 }
121 $this->mComment = $request->getText( 'wpComment' );
122
123 $this->mMerge = $request->wasPosted()
124 && $this->getContext()->getCsrfTokenSet()->matchToken( $request->getVal( 'wpEditToken' ) );
125
126 // target page
127 if ( $this->mSubmitted ) {
128 $this->mTargetObj = Title::newFromText( $this->mTarget );
129 $this->mDestObj = Title::newFromText( $this->mDest );
130 } else {
131 $this->mTargetObj = null;
132 $this->mDestObj = null;
133 }
134 }
135
137 public function execute( $par ) {
139
140 $this->checkPermissions();
141 $this->checkReadOnly();
142
143 $this->loadRequestParams();
144
145 $this->setHeaders();
146 $this->outputHeader();
147 $status = Status::newGood();
148
149 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
150 $this->merge();
151
152 return;
153 }
154
155 if ( !$this->mSubmitted ) {
156 $this->showMergeForm();
157
158 return;
159 }
160
161 if ( !$this->mTargetObj instanceof Title ) {
162 $status->merge( Status::newFatal( 'mergehistory-invalid-source' ) );
163 } elseif ( !$this->mTargetObj->exists() ) {
164 $status->merge( Status::newFatal(
165 'mergehistory-no-source',
166 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
167 ) );
168 }
169
170 if ( !$this->mDestObj instanceof Title ) {
171 $status->merge( Status::newFatal( 'mergehistory-invalid-destination' ) );
172 } elseif ( !$this->mDestObj->exists() ) {
173 $status->merge( Status::newFatal(
174 'mergehistory-no-destination',
175 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
176 ) );
177 }
178
179 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
180 $status->merge( Status::newFatal( 'mergehistory-same-destination' ) );
181 }
182
183 $this->mStatus = $status;
184
185 $this->showMergeForm();
186
187 if ( $this->mStatus->isGood() ) {
188 $this->showHistory();
189 }
190 }
191
192 private function showMergeForm() {
193 $out = $this->getOutput();
194 $out->addWikiMsg( 'mergehistory-header' );
195
196 $fields = [
197 'submitted' => [
198 'type' => 'hidden',
199 'default' => '1',
200 'name' => 'submitted'
201 ],
202 'title' => [
203 'type' => 'hidden',
204 'default' => $this->getPageTitle()->getPrefixedDBkey(),
205 'name' => 'title'
206 ],
207 'mergepoint' => [
208 'type' => 'hidden',
209 'default' => $this->mTimestamp,
210 'name' => 'mergepoint'
211 ],
212 'mergepointold' => [
213 'type' => 'hidden',
214 'default' => $this->mTimestampOld,
215 'name' => 'mergepointold'
216 ],
217 'target' => [
218 'type' => 'title',
219 'label-message' => 'mergehistory-from',
220 'default' => $this->mTarget,
221 'id' => 'target',
222 'name' => 'target'
223 ],
224 'dest' => [
225 'type' => 'title',
226 'label-message' => 'mergehistory-into',
227 'default' => $this->mDest,
228 'id' => 'dest',
229 'name' => 'dest'
230 ]
231 ];
232
233 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
234 $form->setWrapperLegendMsg( 'mergehistory-box' )
235 ->setSubmitTextMsg( 'mergehistory-go' )
236 ->setMethod( 'get' )
237 ->prepareForm()
238 ->displayForm( $this->mStatus );
239
240 $this->addHelpLink( 'Help:Merge history' );
241 }
242
243 private function showHistory() {
244 # List all stored revisions
245 $revisions = new MergeHistoryPager(
246 $this->getContext(),
247 $this->getLinkRenderer(),
248 $this->linkBatchFactory,
249 $this->dbProvider,
250 $this->revisionStore,
251 $this->commentFormatter,
252 $this->changeTagsStore,
253 [],
254 $this->mTargetObj,
255 $this->mDestObj,
256 $this->mTimestamp,
257 $this->mTimestampOld
258 );
259 $haveRevisions = $revisions->getNumRows() > 0;
260
261 $out = $this->getOutput();
262 $out->addModuleStyles( [
263 'mediawiki.interface.helpers.styles',
264 'mediawiki.special'
265 ] );
266 $out->addModules( 'mediawiki.special.mergeHistory' );
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' )->escaped() );
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
356 private function merge() {
357 # Get the titles directly from the IDs, in case the target page params
358 # were spoofed. The queries are done based on the IDs, so it's best to
359 # keep it consistent...
360 $targetTitle = Title::newFromID( $this->mTargetID );
361 $destTitle = Title::newFromID( $this->mDestID );
362 if ( $targetTitle === null || $destTitle === null ) {
363 return false; // validate these
364 }
365 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
366 return false;
367 }
368
369 // MergeHistory object
370 $mh = $this->mergeHistoryFactory->newMergeHistory(
371 $targetTitle,
372 $destTitle,
373 $this->mTimestamp,
374 $this->mTimestampOld
375 );
376
377 // Merge!
378 $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
379 if ( !$mergeStatus->isOK() ) {
380 // Failed merge
381 $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
382 return false;
383 }
384
385 $linkRenderer = $this->getLinkRenderer();
386
387 $targetLink = $linkRenderer->makeLink(
388 $targetTitle,
389 null,
390 [],
391 [ 'redirect' => 'no' ]
392 );
393
394 // In some cases the target page will be deleted
395 $append = ( $mergeStatus->getValue() === 'source-deleted' )
396 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
397
398 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
399 ->rawParams( $targetLink )
400 ->params( $destTitle->getPrefixedText(), $append )
401 ->numParams( $mh->getMergedRevisionCount() )
402 );
403
404 return true;
405 }
406
408 protected function getGroupName() {
409 return 'pagetools';
410 }
411}
412
417class_alias( SpecialMergeHistory::class, 'SpecialMergeHistory' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Read-write access to the change_tags table.
This is the main service interface for converting single-line comments from various DB comment fields...
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:195
Class to simplify the use of log pages.
Definition LogPage.php:35
Service for looking up page revisions.
Parent class for all special pages.
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 By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Combine the revision history of two articles into one.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
__construct(MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, RevisionStore $revisionStore, CommentFormatter $commentFormatter, ChangeTagsStore $changeTagsStore)
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki....
execute( $par)
Default execute method Checks user permissions.This must be overridden by subclasses; it will be made...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Represents a title within MediaWiki.
Definition Title.php:70
Service for mergehistory actions.
Provide primary and replica IDatabase connections.