MediaWiki master
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
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
70 private $mStatus;
71
72 public function __construct(
73 private readonly MergeHistoryFactory $mergeHistoryFactory,
74 private readonly LinkBatchFactory $linkBatchFactory,
75 private readonly IConnectionProvider $dbProvider,
76 private readonly RevisionStore $revisionStore,
77 private readonly CommentFormatter $commentFormatter,
78 private readonly ChangeTagsStore $changeTagsStore,
79 ) {
80 parent::__construct( 'MergeHistory', 'mergehistory' );
81 }
82
84 public function doesWrites() {
85 return true;
86 }
87
91 private function loadRequestParams() {
92 $request = $this->getRequest();
93 $this->mAction = $request->getRawVal( 'action' );
94 $this->mTarget = $request->getVal( 'target', '' );
95 $this->mDest = $request->getVal( 'dest', '' );
96 $this->mSubmitted = $request->getBool( 'submitted' );
97
98 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
99 $this->mDestID = intval( $request->getVal( 'destID' ) );
100 $this->mTimestamp = $request->getVal( 'mergepoint' );
101 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}(\|[0-9]+)?/', $this->mTimestamp ) ) {
102 $this->mTimestamp = '';
103 }
104 $this->mTimestampOld = $request->getVal( 'mergepointold' );
105 if ( $this->mTimestampOld === null || !preg_match( '/[0-9]{14}(\|[0-9]+)?/', $this->mTimestamp ) ) {
106 $this->mTimestampOld = '';
107 }
108 $this->mComment = $request->getText( 'wpComment' );
109
110 $this->mMerge = $request->wasPosted()
111 && $this->getContext()->getCsrfTokenSet()->matchToken( $request->getVal( 'wpEditToken' ) );
112
113 // target page
114 if ( $this->mSubmitted ) {
115 $this->mTargetObj = Title::newFromText( $this->mTarget );
116 $this->mDestObj = Title::newFromText( $this->mDest );
117 } else {
118 $this->mTargetObj = null;
119 $this->mDestObj = null;
120 }
121 }
122
124 public function execute( $par ) {
126
127 $this->checkPermissions();
128 $this->checkReadOnly();
129
130 $this->loadRequestParams();
131
132 $this->setHeaders();
133 $this->outputHeader();
134 $status = Status::newGood();
135
136 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
137 $this->merge();
138
139 return;
140 }
141
142 if ( !$this->mSubmitted ) {
143 $this->showMergeForm();
144
145 return;
146 }
147
148 if ( !$this->mTargetObj instanceof Title ) {
149 $status->merge( Status::newFatal( 'mergehistory-invalid-source' ) );
150 } elseif ( !$this->mTargetObj->exists() ) {
151 $status->merge( Status::newFatal(
152 'mergehistory-no-source',
153 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
154 ) );
155 }
156
157 if ( !$this->mDestObj instanceof Title ) {
158 $status->merge( Status::newFatal( 'mergehistory-invalid-destination' ) );
159 } elseif ( !$this->mDestObj->exists() ) {
160 $status->merge( Status::newFatal(
161 'mergehistory-no-destination',
162 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
163 ) );
164 }
165
166 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
167 $status->merge( Status::newFatal( 'mergehistory-same-destination' ) );
168 }
169
170 $this->mStatus = $status;
171
172 $this->showMergeForm();
173
174 if ( $this->mStatus->isGood() ) {
175 $this->showHistory();
176 }
177 }
178
179 private function showMergeForm() {
180 $out = $this->getOutput();
181 $out->addWikiMsg( 'mergehistory-header' );
182
183 $fields = [
184 'submitted' => [
185 'type' => 'hidden',
186 'default' => '1',
187 'name' => 'submitted'
188 ],
189 'title' => [
190 'type' => 'hidden',
191 'default' => $this->getPageTitle()->getPrefixedDBkey(),
192 'name' => 'title'
193 ],
194 'mergepoint' => [
195 'type' => 'hidden',
196 'default' => $this->mTimestamp,
197 'name' => 'mergepoint'
198 ],
199 'mergepointold' => [
200 'type' => 'hidden',
201 'default' => $this->mTimestampOld,
202 'name' => 'mergepointold'
203 ],
204 'target' => [
205 'type' => 'title',
206 'label-message' => 'mergehistory-from',
207 'default' => $this->mTarget,
208 'id' => 'target',
209 'name' => 'target'
210 ],
211 'dest' => [
212 'type' => 'title',
213 'label-message' => 'mergehistory-into',
214 'default' => $this->mDest,
215 'id' => 'dest',
216 'name' => 'dest'
217 ]
218 ];
219
220 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
221 $form->setWrapperLegendMsg( 'mergehistory-box' )
222 ->setSubmitTextMsg( 'mergehistory-go' )
223 ->setMethod( 'get' )
224 ->prepareForm()
225 ->displayForm( $this->mStatus );
226
227 $this->addHelpLink( 'Help:Merge history' );
228 }
229
230 private function showHistory() {
231 # List all stored revisions
232 $revisions = new MergeHistoryPager(
233 $this->getContext(),
234 $this->getLinkRenderer(),
235 $this->linkBatchFactory,
236 $this->dbProvider,
237 $this->revisionStore,
238 $this->commentFormatter,
239 $this->changeTagsStore,
240 [],
241 $this->mTargetObj,
242 $this->mDestObj,
243 $this->mTimestamp,
244 $this->mTimestampOld
245 );
246 $haveRevisions = $revisions->getNumRows() > 0;
247
248 $out = $this->getOutput();
249 $out->addModuleStyles( [
250 'mediawiki.interface.helpers.styles',
251 'mediawiki.special'
252 ] );
253 $out->addModules( 'mediawiki.special.mergeHistory' );
254 $titleObj = $this->getPageTitle();
255 $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
256 # Start the form here
257 $fields = [
258 'targetID' => [
259 'type' => 'hidden',
260 'name' => 'targetID',
261 'default' => $this->mTargetObj->getArticleID()
262 ],
263 'destID' => [
264 'type' => 'hidden',
265 'name' => 'destID',
266 'default' => $this->mDestObj->getArticleID()
267 ],
268 'target' => [
269 'type' => 'hidden',
270 'name' => 'target',
271 'default' => $this->mTarget
272 ],
273 'dest' => [
274 'type' => 'hidden',
275 'name' => 'dest',
276 'default' => $this->mDest
277 ],
278 ];
279 if ( $haveRevisions ) {
280 $fields += [
281 'explanation' => [
282 'type' => 'info',
283 'default' => $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
284 $this->mDestObj->getPrefixedText() )->parse(),
285 'raw' => true,
286 'cssclass' => 'mw-mergehistory-explanation',
287 'section' => 'mergehistory-submit'
288 ],
289 'reason' => [
290 'type' => 'text',
291 'name' => 'wpComment',
292 'label-message' => 'mergehistory-reason',
293 'size' => 50,
294 'default' => $this->mComment,
295 'section' => 'mergehistory-submit'
296 ],
297 'submit' => [
298 'type' => 'submit',
299 'default' => $this->msg( 'mergehistory-submit' ),
300 'section' => 'mergehistory-submit',
301 'id' => 'mw-merge-submit',
302 'name' => 'merge'
303 ]
304 ];
305 }
306 $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
307 $form->addHiddenField( 'wpEditToken', $form->getCsrfTokenSet()->getToken() )
308 ->setId( 'merge' )
309 ->setAction( $action )
310 ->suppressDefaultSubmit();
311
312 if ( $haveRevisions ) {
313 $form->setFooterHtml(
314 '<h2 id="mw-mergehistory">' . $this->msg( 'mergehistory-list' )->escaped() . '</h2>' .
315 $revisions->getNavigationBar() .
316 $revisions->getBody() .
317 $revisions->getNavigationBar()
318 );
319 } else {
320 $form->setFooterHtml( $this->msg( 'mergehistory-empty' )->escaped() );
321 }
322
323 $form->prepareForm()->displayForm( false );
324
325 # Show relevant lines from the merge log:
326 $mergeLogPage = new LogPage( 'merge' );
327 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
328 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
329 }
330
343 private function merge() {
344 # Get the titles directly from the IDs, in case the target page params
345 # were spoofed. The queries are done based on the IDs, so it's best to
346 # keep it consistent...
347 $targetTitle = Title::newFromID( $this->mTargetID );
348 $destTitle = Title::newFromID( $this->mDestID );
349 if ( $targetTitle === null || $destTitle === null ) {
350 return false; // validate these
351 }
352 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
353 return false;
354 }
355
356 // MergeHistory object
357 $mh = $this->mergeHistoryFactory->newMergeHistory(
358 $targetTitle,
359 $destTitle,
360 $this->mTimestamp,
361 $this->mTimestampOld
362 );
363
364 // Merge!
365 $mergeStatus = $mh->merge( $this->getAuthority(), $this->mComment );
366 if ( !$mergeStatus->isOK() ) {
367 // Failed merge
368 $this->getOutput()->addWikiMsg( Status::wrap( $mergeStatus )->getMessage() );
369 return false;
370 }
371
372 $linkRenderer = $this->getLinkRenderer();
373
374 $targetLink = $linkRenderer->makeLink(
375 $targetTitle,
376 null,
377 [],
378 [ 'redirect' => 'no' ]
379 );
380
381 // In some cases the target page will be deleted
382 $append = ( $mergeStatus->getValue() === 'source-deleted' )
383 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
384
385 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
386 ->rawParams( $targetLink )
387 ->params( $destTitle->getPrefixedText(), $append )
388 ->numParams( $mh->getMergedRevisionCount() )
389 );
390
391 return true;
392 }
393
395 protected function getGroupName() {
396 return 'pagetools';
397 }
398}
399
404class_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:207
Class to simplify the use of log pages.
Definition LogPage.php:35
Factory for LinkBatch objects to batch query page metadata.
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...
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...
__construct(private readonly MergeHistoryFactory $mergeHistoryFactory, private readonly LinkBatchFactory $linkBatchFactory, private readonly IConnectionProvider $dbProvider, private readonly RevisionStore $revisionStore, private readonly CommentFormatter $commentFormatter, private readonly ChangeTagsStore $changeTagsStore,)
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:69
Service for mergehistory actions.
Provide primary and replica IDatabase connections.