MediaWiki master
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Specials;
25
27use 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}(\|[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:44
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:206
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 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:54
Represents a title within MediaWiki.
Definition Title.php:78
Service for mergehistory actions.
Provide primary and replica IDatabase connections.
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...