MediaWiki REL1_37
SpecialMergeHistory.php
Go to the documentation of this file.
1<?php
29
38 protected $mAction;
39
41 protected $mTarget;
42
44 protected $mDest;
45
47 protected $mTimestamp;
48
50 protected $mTargetID;
51
53 protected $mDestID;
54
56 protected $mComment;
57
59 protected $mMerge;
60
62 protected $mSubmitted;
63
65 protected $mTargetObj;
66
68 protected $mDestObj;
69
71 public $prevId;
72
75
78
81
84
91 public function __construct(
96 ) {
97 parent::__construct( 'MergeHistory', 'mergehistory' );
98 $this->mergeHistoryFactory = $mergeHistoryFactory;
99 $this->linkBatchFactory = $linkBatchFactory;
100 $this->loadBalancer = $loadBalancer;
101 $this->revisionStore = $revisionStore;
102 }
103
104 public function doesWrites() {
105 return true;
106 }
107
111 private function loadRequestParams() {
112 $request = $this->getRequest();
113 $this->mAction = $request->getRawVal( 'action' );
114 $this->mTarget = $request->getVal( 'target', '' );
115 $this->mDest = $request->getVal( 'dest', '' );
116 $this->mSubmitted = $request->getBool( 'submitted' );
117
118 $this->mTargetID = intval( $request->getVal( 'targetID' ) );
119 $this->mDestID = intval( $request->getVal( 'destID' ) );
120 $this->mTimestamp = $request->getVal( 'mergepoint' );
121 if ( $this->mTimestamp === null || !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
122 $this->mTimestamp = '';
123 }
124 $this->mComment = $request->getText( 'wpComment' );
125
126 $this->mMerge = $request->wasPosted()
127 && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
128
129 // target page
130 if ( $this->mSubmitted ) {
131 $this->mTargetObj = Title::newFromText( $this->mTarget );
132 $this->mDestObj = Title::newFromText( $this->mDest );
133 } else {
134 $this->mTargetObj = null;
135 $this->mDestObj = null;
136 }
137 }
138
139 public function execute( $par ) {
141
142 $this->checkPermissions();
143 $this->checkReadOnly();
144
145 $this->loadRequestParams();
146
147 $this->setHeaders();
148 $this->outputHeader();
149
150 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
151 $this->merge();
152
153 return;
154 }
155
156 if ( !$this->mSubmitted ) {
157 $this->showMergeForm();
158
159 return;
160 }
161
162 $errors = [];
163 if ( !$this->mTargetObj instanceof Title ) {
164 $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
165 } elseif ( !$this->mTargetObj->exists() ) {
166 $errors[] = $this->msg( 'mergehistory-no-source',
167 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
168 )->parseAsBlock();
169 }
170
171 if ( !$this->mDestObj instanceof Title ) {
172 $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
173 } elseif ( !$this->mDestObj->exists() ) {
174 $errors[] = $this->msg( 'mergehistory-no-destination',
175 wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
176 )->parseAsBlock();
177 }
178
179 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
180 $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
181 }
182
183 if ( count( $errors ) ) {
184 $this->showMergeForm();
185 $this->getOutput()->addHTML( implode( "\n", $errors ) );
186 } else {
187 $this->showHistory();
188 }
189 }
190
191 private function showMergeForm() {
192 $out = $this->getOutput();
193 $out->addWikiMsg( 'mergehistory-header' );
194
195 $out->addHTML(
196 Xml::openElement( 'form', [
197 'method' => 'get',
198 'action' => wfScript() ] ) .
199 '<fieldset>' .
200 Xml::element( 'legend', [],
201 $this->msg( 'mergehistory-box' )->text() ) .
202 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
203 Html::hidden( 'submitted', '1' ) .
204 Html::hidden( 'mergepoint', $this->mTimestamp ) .
205 Xml::openElement( 'table' ) .
206 '<tr>
207 <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
208 <td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td>
209 </tr><tr>
210 <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
211 <td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td>
212 </tr><tr><td>' .
213 Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
214 '</td></tr>' .
215 Xml::closeElement( 'table' ) .
216 '</fieldset>' .
217 '</form>'
218 );
219
220 $this->addHelpLink( 'Help:Merge history' );
221 }
222
223 private function showHistory() {
224 $this->showMergeForm();
225
226 # List all stored revisions
227 $revisions = new MergeHistoryPager(
228 $this,
229 [],
230 $this->mTargetObj,
231 $this->mDestObj,
232 $this->linkBatchFactory,
233 $this->loadBalancer,
234 $this->revisionStore
235 );
236 $haveRevisions = $revisions->getNumRows() > 0;
237
238 $out = $this->getOutput();
239 $titleObj = $this->getPageTitle();
240 $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
241 # Start the form here
242 $top = Xml::openElement(
243 'form',
244 [
245 'method' => 'post',
246 'action' => $action,
247 'id' => 'merge'
248 ]
249 );
250 $out->addHTML( $top );
251
252 if ( $haveRevisions ) {
253 # Format the user-visible controls (comment field, submission button)
254 # in a nice little table
255 $table =
256 Xml::openElement( 'fieldset' ) .
257 $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
258 $this->mDestObj->getPrefixedText() )->parse() .
259 Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) .
260 '<tr>
261 <td class="mw-label">' .
262 Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
263 '</td>
264 <td class="mw-input">' .
265 Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) .
266 "</td>
267 </tr>
268 <tr>
269 <td>\u{00A0}</td>
270 <td class=\"mw-submit\">" .
271 Xml::submitButton(
272 $this->msg( 'mergehistory-submit' )->text(),
273 [ 'name' => 'merge', 'id' => 'mw-merge-submit' ]
274 ) .
275 '</td>
276 </tr>' .
277 Xml::closeElement( 'table' ) .
278 Xml::closeElement( 'fieldset' );
279
280 $out->addHTML( $table );
281 }
282
283 $out->addHTML(
284 '<h2 id="mw-mergehistory">' .
285 $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
286 );
287
288 if ( $haveRevisions ) {
289 $out->addHTML( $revisions->getNavigationBar() );
290 $out->addHTML( '<ul>' );
291 $out->addHTML( $revisions->getBody() );
292 $out->addHTML( '</ul>' );
293 $out->addHTML( $revisions->getNavigationBar() );
294 } else {
295 $out->addWikiMsg( 'mergehistory-empty' );
296 }
297
298 # Show relevant lines from the merge log:
299 $mergeLogPage = new LogPage( 'merge' );
300 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
301 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
302
303 # When we submit, go by page ID to avoid some nasty but unlikely collisions.
304 # Such would happen if a page was renamed after the form loaded, but before submit
305 $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
306 $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
307 $misc .= Html::hidden( 'target', $this->mTarget );
308 $misc .= Html::hidden( 'dest', $this->mDest );
309 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
310 $misc .= Xml::closeElement( 'form' );
311 $out->addHTML( $misc );
312
313 return true;
314 }
315
316 public function formatRevisionRow( $row ) {
317 $revRecord = $this->revisionStore->newRevisionFromRow( $row );
318
320
321 $stxt = '';
322 $last = $this->msg( 'last' )->escaped();
323
324 $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
325 $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
326
327 $user = $this->getUser();
328
329 $pageLink = $linkRenderer->makeKnownLink(
330 $revRecord->getPageAsLinkTarget(),
331 $this->getLanguage()->userTimeAndDate( $ts, $user ),
332 [],
333 [ 'oldid' => $revRecord->getId() ]
334 );
335 if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
336 $class = Linker::getRevisionDeletedClass( $revRecord );
337 $pageLink = '<span class=" ' . $class . '">' . $pageLink . '</span>';
338 }
339
340 # Last link
341 if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
342 $last = $this->msg( 'last' )->escaped();
343 } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
345 $revRecord->getPageAsLinkTarget(),
346 $this->msg( 'last' )->text(),
347 [],
348 [
349 'diff' => $row->rev_id,
350 'oldid' => $this->prevId[$row->rev_id]
351 ]
352 );
353 }
354
355 $userLink = Linker::revUserTools( $revRecord );
356
357 $size = $row->rev_len;
358 if ( $size !== null ) {
359 $stxt = Linker::formatRevisionSize( $size );
360 }
361 $comment = Linker::revComment( $revRecord );
362
363 return Html::rawElement( 'li', [],
364 $this->msg( 'mergehistory-revisionrow' )
365 ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
366 }
367
380 private function merge() {
381 # Get the titles directly from the IDs, in case the target page params
382 # were spoofed. The queries are done based on the IDs, so it's best to
383 # keep it consistent...
384 $targetTitle = Title::newFromID( $this->mTargetID );
385 $destTitle = Title::newFromID( $this->mDestID );
386 if ( $targetTitle === null || $destTitle === null ) {
387 return false; // validate these
388 }
389 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
390 return false;
391 }
392
393 // MergeHistory object
394 $mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
395
396 // Merge!
397 $mergeStatus = $mh->merge( $this->getUser(), $this->mComment );
398 if ( !$mergeStatus->isOK() ) {
399 // Failed merge
400 $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
401 return false;
402 }
403
405
406 $targetLink = $linkRenderer->makeLink(
407 $targetTitle,
408 null,
409 [],
410 [ 'redirect' => 'no' ]
411 );
412
413 // In some cases the target page will be deleted
414 $append = ( $mergeStatus->getValue() === 'source-deleted' )
415 ? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
416
417 $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
418 ->rawParams( $targetLink )
419 ->params( $destTitle->getPrefixedText(), $append )
420 ->numParams( $mh->getMergedRevisionCount() )
421 );
422
423 return true;
424 }
425
426 protected function getGroupName() {
427 return 'pagetools';
428 }
429}
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition Linker.php:1299
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1782
static formatRevisionSize( $size)
Definition Linker.php:1820
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1319
Class to simplify the use of log pages.
Definition LogPage.php:38
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Page revision base class.
Service for looking up page revisions.
Special page allowing users with the appropriate permissions to merge article histories,...
__construct(MergeHistoryFactory $mergeHistoryFactory, LinkBatchFactory $linkBatchFactory, ILoadBalancer $loadBalancer, RevisionStore $revisionStore)
merge()
Actually attempt the history move.
doesWrites()
Indicates whether this special page may perform database writes.
MergeHistoryFactory $mergeHistoryFactory
bool $mSubmitted
Was submitted?
execute( $par)
Default execute method Checks user permissions.
LinkBatchFactory $linkBatchFactory
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Parent class for all special pages.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
checkPermissions()
Checks if userCanExecute, and if not throws a PermissionsError.
LinkRenderer null $linkRenderer
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest being used for this instance.
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
getPageTitle( $subpage=false)
Get a self-referential title object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Represents a title within MediaWiki.
Definition Title.php:48
Database cluster connection, tracking, load balancing, and transaction manager interface.