Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 212 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
SpecialMergeHistory | |
0.00% |
0 / 211 |
|
0.00% |
0 / 8 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
loadRequestParams | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
execute | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
210 | |||
showMergeForm | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
2 | |||
showHistory | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
12 | |||
merge | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Implements Special:MergeHistory |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | * @ingroup SpecialPage |
22 | */ |
23 | |
24 | namespace MediaWiki\Specials; |
25 | |
26 | use LogEventsList; |
27 | use LogPage; |
28 | use MediaWiki\Cache\LinkBatchFactory; |
29 | use MediaWiki\CommentFormatter\CommentFormatter; |
30 | use MediaWiki\HTMLForm\HTMLForm; |
31 | use MediaWiki\Page\MergeHistoryFactory; |
32 | use MediaWiki\Pager\MergeHistoryPager; |
33 | use MediaWiki\Revision\RevisionStore; |
34 | use MediaWiki\SpecialPage\SpecialPage; |
35 | use MediaWiki\Status\Status; |
36 | use MediaWiki\Title\Title; |
37 | use Wikimedia\Rdbms\IConnectionProvider; |
38 | |
39 | /** |
40 | * Special page allowing users with the appropriate permissions to |
41 | * merge article histories, with some restrictions |
42 | * |
43 | * @ingroup SpecialPage |
44 | */ |
45 | class SpecialMergeHistory extends SpecialPage { |
46 | /** @var string */ |
47 | protected $mAction; |
48 | |
49 | /** @var string */ |
50 | protected $mTarget; |
51 | |
52 | /** @var string */ |
53 | protected $mDest; |
54 | |
55 | /** @var string */ |
56 | protected $mTimestamp; |
57 | |
58 | /** @var int */ |
59 | protected $mTargetID; |
60 | |
61 | /** @var int */ |
62 | protected $mDestID; |
63 | |
64 | /** @var string */ |
65 | protected $mComment; |
66 | |
67 | /** @var bool Was posted? */ |
68 | protected $mMerge; |
69 | |
70 | /** @var bool Was submitted? */ |
71 | protected $mSubmitted; |
72 | |
73 | /** @var Title|null */ |
74 | protected $mTargetObj; |
75 | |
76 | /** @var Title|null */ |
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 | |
85 | /** @var Status */ |
86 | private $mStatus; |
87 | |
88 | /** |
89 | * @param MergeHistoryFactory $mergeHistoryFactory |
90 | * @param LinkBatchFactory $linkBatchFactory |
91 | * @param IConnectionProvider $dbProvider |
92 | * @param RevisionStore $revisionStore |
93 | * @param CommentFormatter $commentFormatter |
94 | */ |
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 | |
114 | /** |
115 | * @return void |
116 | */ |
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 ) { |
146 | $this->useTransactionalTimeLimit(); |
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 | |
346 | /** |
347 | * Actually attempt the history move |
348 | * |
349 | * @todo if all versions of page A are moved to B and then a user |
350 | * tries to do a reverse-merge via the "unmerge" log link, then page |
351 | * A will still be a redirect (as it was after the original merge), |
352 | * though it will have the old revisions back from before (as expected). |
353 | * The user may have to "undo" the redirect manually to finish the "unmerge". |
354 | * Maybe this should delete redirects at the target page of merges? |
355 | * |
356 | * @return bool Success |
357 | */ |
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 | |
409 | /** |
410 | * Retain the old class name for backwards compatibility. |
411 | * @deprecated since 1.41 |
412 | */ |
413 | class_alias( SpecialMergeHistory::class, 'SpecialMergeHistory' ); |