MediaWiki master
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
24use LogPage;
35
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}(\|[0-9]+)?/', $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 ) {
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
413class_alias( SpecialMergeHistory::class, 'SpecialMergeHistory' );
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:46
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:209
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.
__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:54
Represents a title within MediaWiki.
Definition Title.php:78
Service for mergehistory actions.
Provide primary and replica IDatabase connections.