MediaWiki master
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
36
48 protected $mAction;
49
51 protected $mTarget;
52
54 protected $mDest;
55
57 protected $mTimestamp;
58
60 protected $mTargetID;
61
63 protected $mDestID;
64
66 protected $mComment;
67
69 protected $mMerge;
70
72 protected $mSubmitted;
73
75 protected $mTargetObj;
76
78 protected $mDestObj;
79
80 private MergeHistoryFactory $mergeHistoryFactory;
81 private LinkBatchFactory $linkBatchFactory;
82 private IConnectionProvider $dbProvider;
83 private RevisionStore $revisionStore;
84 private CommentFormatter $commentFormatter;
85 private ChangeTagsStore $changeTagsStore;
86
88 private $mStatus;
89
90 public function __construct(
91 MergeHistoryFactory $mergeHistoryFactory,
92 LinkBatchFactory $linkBatchFactory,
93 IConnectionProvider $dbProvider,
94 RevisionStore $revisionStore,
95 CommentFormatter $commentFormatter,
96 ChangeTagsStore $changeTagsStore
97 ) {
98 parent::__construct( 'MergeHistory', 'mergehistory' );
99 $this->mergeHistoryFactory = $mergeHistoryFactory;
100 $this->linkBatchFactory = $linkBatchFactory;
101 $this->dbProvider = $dbProvider;
102 $this->revisionStore = $revisionStore;
103 $this->commentFormatter = $commentFormatter;
104 $this->changeTagsStore = $changeTagsStore;
105 }
106
107 public function doesWrites() {
108 return true;
109 }
110
114 private function loadRequestParams() {
115 $request = $this->getRequest();
116 $this->mAction = $request->getRawVal( 'action' );
117 $this->mTarget = $request->getVal( 'target', '' );
118 $this->mDest = $request->getVal( 'dest', '' );
119 $this->mSubmitted = $request->getBool( 'submitted' );
120
121 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
122 $this->mDestID = intval( $request->getVal( 'destID' ) );
123 $this->mTimestamp = $request->getVal( 'mergepoint' );
124 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}(\|[0-9]+)?/', $this->mTimestamp ) ) {
125 $this->mTimestamp = '';
126 }
127 $this->mComment = $request->getText( 'wpComment' );
128
129 $this->mMerge = $request->wasPosted()
130 && $this->getContext()->getCsrfTokenSet()->matchToken( $request->getVal( 'wpEditToken' ) );
131
132 // target page
133 if ( $this->mSubmitted ) {
134 $this->mTargetObj = Title::newFromText( $this->mTarget );
135 $this->mDestObj = Title::newFromText( $this->mDest );
136 } else {
137 $this->mTargetObj = null;
138 $this->mDestObj = null;
139 }
140 }
141
142 public function execute( $par ) {
144
145 $this->checkPermissions();
146 $this->checkReadOnly();
147
148 $this->loadRequestParams();
149
150 $this->setHeaders();
151 $this->outputHeader();
152 $status = Status::newGood();
153
154 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
155 $this->merge();
156
157 return;
158 }
159
160 if ( !$this->mSubmitted ) {
161 $this->showMergeForm();
162
163 return;
164 }
165
166 if ( !$this->mTargetObj instanceof Title ) {
167 $status->merge( Status::newFatal( 'mergehistory-invalid-source' ) );
168 } elseif ( !$this->mTargetObj->exists() ) {
169 $status->merge( Status::newFatal(
170 'mergehistory-no-source',
171 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
172 ) );
173 }
174
175 if ( !$this->mDestObj instanceof Title ) {
176 $status->merge( Status::newFatal( 'mergehistory-invalid-destination' ) );
177 } elseif ( !$this->mDestObj->exists() ) {
178 $status->merge( Status::newFatal(
179 'mergehistory-no-destination',
180 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
181 ) );
182 }
183
184 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
185 $status->merge( Status::newFatal( 'mergehistory-same-destination' ) );
186 }
187
188 $this->mStatus = $status;
189
190 $this->showMergeForm();
191
192 if ( $this->mStatus->isGood() ) {
193 $this->showHistory();
194 }
195 }
196
197 private function showMergeForm() {
198 $out = $this->getOutput();
199 $out->addWikiMsg( 'mergehistory-header' );
200
201 $fields = [
202 'submitted' => [
203 'type' => 'hidden',
204 'default' => '1',
205 'name' => 'submitted'
206 ],
207 'title' => [
208 'type' => 'hidden',
209 'default' => $this->getPageTitle()->getPrefixedDBkey(),
210 'name' => 'title'
211 ],
212 'mergepoint' => [
213 'type' => 'hidden',
214 'default' => $this->mTimestamp,
215 'name' => 'mergepoint'
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 );
258 $haveRevisions = $revisions->getNumRows() > 0;
259
260 $out = $this->getOutput();
261 $out->addModuleStyles( [
262 'mediawiki.interface.helpers.styles',
263 'mediawiki.special'
264 ] );
265 $titleObj = $this->getPageTitle();
266 $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
267 # Start the form here
268 $fields = [
269 'targetID' => [
270 'type' => 'hidden',
271 'name' => 'targetID',
272 'default' => $this->mTargetObj->getArticleID()
273 ],
274 'destID' => [
275 'type' => 'hidden',
276 'name' => 'destID',
277 'default' => $this->mDestObj->getArticleID()
278 ],
279 'target' => [
280 'type' => 'hidden',
281 'name' => 'target',
282 'default' => $this->mTarget
283 ],
284 'dest' => [
285 'type' => 'hidden',
286 'name' => 'dest',
287 'default' => $this->mDest
288 ],
289 ];
290 if ( $haveRevisions ) {
291 $fields += [
292 'explanation' => [
293 'type' => 'info',
294 'default' => $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
295 $this->mDestObj->getPrefixedText() )->parse(),
296 'raw' => true,
297 'cssclass' => 'mw-mergehistory-explanation',
298 'section' => 'mergehistory-submit'
299 ],
300 'reason' => [
301 'type' => 'text',
302 'name' => 'wpComment',
303 'label-message' => 'mergehistory-reason',
304 'size' => 50,
305 'default' => $this->mComment,
306 'section' => 'mergehistory-submit'
307 ],
308 'submit' => [
309 'type' => 'submit',
310 'default' => $this->msg( 'mergehistory-submit' ),
311 'section' => 'mergehistory-submit',
312 'id' => 'mw-merge-submit',
313 'name' => 'merge'
314 ]
315 ];
316 }
317 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
318 $form->addHiddenField( 'wpEditToken', $form->getCsrfTokenSet()->getToken() )
319 ->setId( 'merge' )
320 ->setAction( $action )
321 ->suppressDefaultSubmit();
322
323 if ( $haveRevisions ) {
324 $form->setFooterHtml(
325 '<h2 id="mw-mergehistory">' . $this->msg( 'mergehistory-list' )->escaped() . '</h2>' .
326 $revisions->getNavigationBar() .
327 $revisions->getBody() .
328 $revisions->getNavigationBar()
329 );
330 } else {
331 $form->setFooterHtml( $this->msg( 'mergehistory-empty' ) );
332 }
333
334 $form->prepareForm()->displayForm( false );
335
336 # Show relevant lines from the merge log:
337 $mergeLogPage = new LogPage( 'merge' );
338 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
339 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
340 }
341
354 private function merge() {
355 # Get the titles directly from the IDs, in case the target page params
356 # were spoofed. The queries are done based on the IDs, so it's best to
357 # keep it consistent...
358 $targetTitle = Title::newFromID( $this->mTargetID );
359 $destTitle = Title::newFromID( $this->mDestID );
360 if ( $targetTitle === null || $destTitle === null ) {
361 return false; // validate these
362 }
363 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
364 return false;
365 }
366
367 // MergeHistory object
368 $mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
369
370 // Merge!
371 $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
372 if ( !$mergeStatus->isOK() ) {
373 // Failed merge
374 $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
375 return false;
376 }
377
378 $linkRenderer = $this->getLinkRenderer();
379
380 $targetLink = $linkRenderer->makeLink(
381 $targetTitle,
382 null,
383 [],
384 [ 'redirect' => 'no' ]
385 );
386
387 // In some cases the target page will be deleted
388 $append = ( $mergeStatus->getValue() === 'source-deleted' )
389 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
390
391 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
392 ->rawParams( $targetLink )
393 ->params( $destTitle->getPrefixedText(), $append )
394 ->numParams( $mh->getMergedRevisionCount() )
395 );
396
397 return true;
398 }
399
400 protected function getGroupName() {
401 return 'pagetools';
402 }
403}
404
409class_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:210
Class to simplify the use of log pages.
Definition LogPage.php:50
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
Service for mergehistory actions.
Provide primary and replica IDatabase connections.