This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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 | /** |
||
25 | * Special page allowing users with the appropriate permissions to |
||
26 | * merge article histories, with some restrictions |
||
27 | * |
||
28 | * @ingroup SpecialPage |
||
29 | */ |
||
30 | class SpecialMergeHistory extends SpecialPage { |
||
31 | /** @var string */ |
||
32 | protected $mAction; |
||
33 | |||
34 | /** @var string */ |
||
35 | protected $mTarget; |
||
36 | |||
37 | /** @var string */ |
||
38 | protected $mDest; |
||
39 | |||
40 | /** @var string */ |
||
41 | protected $mTimestamp; |
||
42 | |||
43 | /** @var int */ |
||
44 | protected $mTargetID; |
||
45 | |||
46 | /** @var int */ |
||
47 | protected $mDestID; |
||
48 | |||
49 | /** @var string */ |
||
50 | protected $mComment; |
||
51 | |||
52 | /** @var bool Was posted? */ |
||
53 | protected $mMerge; |
||
54 | |||
55 | /** @var bool Was submitted? */ |
||
56 | protected $mSubmitted; |
||
57 | |||
58 | /** @var Title */ |
||
59 | protected $mTargetObj; |
||
60 | |||
61 | /** @var Title */ |
||
62 | protected $mDestObj; |
||
63 | |||
64 | /** @var int[] */ |
||
65 | public $prevId; |
||
66 | |||
67 | public function __construct() { |
||
68 | parent::__construct( 'MergeHistory', 'mergehistory' ); |
||
69 | } |
||
70 | |||
71 | public function doesWrites() { |
||
72 | return true; |
||
73 | } |
||
74 | |||
75 | /** |
||
76 | * @return void |
||
77 | */ |
||
78 | private function loadRequestParams() { |
||
79 | $request = $this->getRequest(); |
||
80 | $this->mAction = $request->getVal( 'action' ); |
||
81 | $this->mTarget = $request->getVal( 'target' ); |
||
82 | $this->mDest = $request->getVal( 'dest' ); |
||
83 | $this->mSubmitted = $request->getBool( 'submitted' ); |
||
84 | |||
85 | $this->mTargetID = intval( $request->getVal( 'targetID' ) ); |
||
86 | $this->mDestID = intval( $request->getVal( 'destID' ) ); |
||
87 | $this->mTimestamp = $request->getVal( 'mergepoint' ); |
||
88 | if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) { |
||
89 | $this->mTimestamp = ''; |
||
90 | } |
||
91 | $this->mComment = $request->getText( 'wpComment' ); |
||
92 | |||
93 | $this->mMerge = $request->wasPosted() |
||
94 | && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ); |
||
95 | |||
96 | // target page |
||
97 | if ( $this->mSubmitted ) { |
||
98 | $this->mTargetObj = Title::newFromText( $this->mTarget ); |
||
99 | $this->mDestObj = Title::newFromText( $this->mDest ); |
||
100 | } else { |
||
101 | $this->mTargetObj = null; |
||
102 | $this->mDestObj = null; |
||
103 | } |
||
104 | } |
||
105 | |||
106 | public function execute( $par ) { |
||
107 | $this->useTransactionalTimeLimit(); |
||
108 | |||
109 | $this->checkPermissions(); |
||
110 | $this->checkReadOnly(); |
||
111 | |||
112 | $this->loadRequestParams(); |
||
113 | |||
114 | $this->setHeaders(); |
||
115 | $this->outputHeader(); |
||
116 | |||
117 | if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) { |
||
118 | $this->merge(); |
||
119 | |||
120 | return; |
||
121 | } |
||
122 | |||
123 | if ( !$this->mSubmitted ) { |
||
124 | $this->showMergeForm(); |
||
125 | |||
126 | return; |
||
127 | } |
||
128 | |||
129 | $errors = []; |
||
130 | View Code Duplication | if ( !$this->mTargetObj instanceof Title ) { |
|
131 | $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock(); |
||
132 | } elseif ( !$this->mTargetObj->exists() ) { |
||
133 | $errors[] = $this->msg( 'mergehistory-no-source', |
||
134 | wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) |
||
135 | )->parseAsBlock(); |
||
136 | } |
||
137 | |||
138 | View Code Duplication | if ( !$this->mDestObj instanceof Title ) { |
|
139 | $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock(); |
||
140 | } elseif ( !$this->mDestObj->exists() ) { |
||
141 | $errors[] = $this->msg( 'mergehistory-no-destination', |
||
142 | wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) |
||
143 | )->parseAsBlock(); |
||
144 | } |
||
145 | |||
146 | if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) { |
||
147 | $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock(); |
||
148 | } |
||
149 | |||
150 | if ( count( $errors ) ) { |
||
151 | $this->showMergeForm(); |
||
152 | $this->getOutput()->addHTML( implode( "\n", $errors ) ); |
||
153 | } else { |
||
154 | $this->showHistory(); |
||
155 | } |
||
156 | } |
||
157 | |||
158 | function showMergeForm() { |
||
159 | $out = $this->getOutput(); |
||
160 | $out->addWikiMsg( 'mergehistory-header' ); |
||
161 | |||
162 | $out->addHTML( |
||
163 | Xml::openElement( 'form', [ |
||
164 | 'method' => 'get', |
||
165 | 'action' => wfScript() ] ) . |
||
166 | '<fieldset>' . |
||
167 | Xml::element( 'legend', [], |
||
168 | $this->msg( 'mergehistory-box' )->text() ) . |
||
169 | Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . |
||
170 | Html::hidden( 'submitted', '1' ) . |
||
171 | Html::hidden( 'mergepoint', $this->mTimestamp ) . |
||
172 | Xml::openElement( 'table' ) . |
||
173 | '<tr> |
||
174 | <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td> |
||
175 | <td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td> |
||
176 | </tr><tr> |
||
177 | <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td> |
||
178 | <td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td> |
||
179 | </tr><tr><td>' . |
||
180 | Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) . |
||
181 | '</td></tr>' . |
||
182 | Xml::closeElement( 'table' ) . |
||
183 | '</fieldset>' . |
||
184 | '</form>' |
||
185 | ); |
||
186 | |||
187 | $this->addHelpLink( 'Help:Merge history' ); |
||
188 | } |
||
189 | |||
190 | private function showHistory() { |
||
191 | $this->showMergeForm(); |
||
192 | |||
193 | # List all stored revisions |
||
194 | $revisions = new MergeHistoryPager( |
||
195 | $this, [], $this->mTargetObj, $this->mDestObj |
||
196 | ); |
||
197 | $haveRevisions = $revisions && $revisions->getNumRows() > 0; |
||
198 | |||
199 | $out = $this->getOutput(); |
||
200 | $titleObj = $this->getPageTitle(); |
||
201 | $action = $titleObj->getLocalURL( [ 'action' => 'submit' ] ); |
||
202 | # Start the form here |
||
203 | $top = Xml::openElement( |
||
204 | 'form', |
||
205 | [ |
||
206 | 'method' => 'post', |
||
207 | 'action' => $action, |
||
208 | 'id' => 'merge' |
||
209 | ] |
||
210 | ); |
||
211 | $out->addHTML( $top ); |
||
212 | |||
213 | if ( $haveRevisions ) { |
||
214 | # Format the user-visible controls (comment field, submission button) |
||
215 | # in a nice little table |
||
216 | $table = |
||
217 | Xml::openElement( 'fieldset' ) . |
||
218 | $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(), |
||
219 | $this->mDestObj->getPrefixedText() )->parse() . |
||
220 | Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) . |
||
221 | '<tr> |
||
222 | <td class="mw-label">' . |
||
223 | Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) . |
||
224 | '</td> |
||
225 | <td class="mw-input">' . |
||
226 | Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) . |
||
227 | '</td> |
||
228 | </tr> |
||
229 | <tr> |
||
230 | <td> </td> |
||
231 | <td class="mw-submit">' . |
||
232 | Xml::submitButton( |
||
233 | $this->msg( 'mergehistory-submit' )->text(), |
||
234 | [ 'name' => 'merge', 'id' => 'mw-merge-submit' ] |
||
235 | ) . |
||
236 | '</td> |
||
237 | </tr>' . |
||
238 | Xml::closeElement( 'table' ) . |
||
239 | Xml::closeElement( 'fieldset' ); |
||
240 | |||
241 | $out->addHTML( $table ); |
||
242 | } |
||
243 | |||
244 | $out->addHTML( |
||
245 | '<h2 id="mw-mergehistory">' . |
||
246 | $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n" |
||
247 | ); |
||
248 | |||
249 | if ( $haveRevisions ) { |
||
250 | $out->addHTML( $revisions->getNavigationBar() ); |
||
251 | $out->addHTML( '<ul>' ); |
||
252 | $out->addHTML( $revisions->getBody() ); |
||
253 | $out->addHTML( '</ul>' ); |
||
254 | $out->addHTML( $revisions->getNavigationBar() ); |
||
255 | } else { |
||
256 | $out->addWikiMsg( 'mergehistory-empty' ); |
||
257 | } |
||
258 | |||
259 | # Show relevant lines from the merge log: |
||
260 | $mergeLogPage = new LogPage( 'merge' ); |
||
261 | $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" ); |
||
262 | LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj ); |
||
263 | |||
264 | # When we submit, go by page ID to avoid some nasty but unlikely collisions. |
||
265 | # Such would happen if a page was renamed after the form loaded, but before submit |
||
266 | $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() ); |
||
267 | $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() ); |
||
268 | $misc .= Html::hidden( 'target', $this->mTarget ); |
||
269 | $misc .= Html::hidden( 'dest', $this->mDest ); |
||
270 | $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); |
||
271 | $misc .= Xml::closeElement( 'form' ); |
||
272 | $out->addHTML( $misc ); |
||
273 | |||
274 | return true; |
||
275 | } |
||
276 | |||
277 | function formatRevisionRow( $row ) { |
||
278 | $rev = new Revision( $row ); |
||
279 | |||
280 | $stxt = ''; |
||
281 | $last = $this->msg( 'last' )->escaped(); |
||
282 | |||
283 | $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); |
||
284 | $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) ); |
||
0 ignored issues
–
show
|
|||
285 | |||
286 | $user = $this->getUser(); |
||
287 | |||
288 | $pageLink = Linker::linkKnown( |
||
289 | $rev->getTitle(), |
||
290 | htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ), |
||
291 | [], |
||
292 | [ 'oldid' => $rev->getId() ] |
||
293 | ); |
||
294 | if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { |
||
295 | $pageLink = '<span class="history-deleted">' . $pageLink . '</span>'; |
||
296 | } |
||
297 | |||
298 | # Last link |
||
299 | if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { |
||
300 | $last = $this->msg( 'last' )->escaped(); |
||
301 | } elseif ( isset( $this->prevId[$row->rev_id] ) ) { |
||
302 | $last = Linker::linkKnown( |
||
303 | $rev->getTitle(), |
||
304 | $this->msg( 'last' )->escaped(), |
||
305 | [], |
||
306 | [ |
||
307 | 'diff' => $row->rev_id, |
||
308 | 'oldid' => $this->prevId[$row->rev_id] |
||
309 | ] |
||
310 | ); |
||
311 | } |
||
312 | |||
313 | $userLink = Linker::revUserTools( $rev ); |
||
314 | |||
315 | $size = $row->rev_len; |
||
316 | if ( !is_null( $size ) ) { |
||
317 | $stxt = Linker::formatRevisionSize( $size ); |
||
318 | } |
||
319 | $comment = Linker::revComment( $rev ); |
||
320 | |||
321 | return Html::rawElement( 'li', [], |
||
322 | $this->msg( 'mergehistory-revisionrow' ) |
||
323 | ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); |
||
324 | } |
||
325 | |||
326 | /** |
||
327 | * Actually attempt the history move |
||
328 | * |
||
329 | * @todo if all versions of page A are moved to B and then a user |
||
330 | * tries to do a reverse-merge via the "unmerge" log link, then page |
||
331 | * A will still be a redirect (as it was after the original merge), |
||
332 | * though it will have the old revisions back from before (as expected). |
||
333 | * The user may have to "undo" the redirect manually to finish the "unmerge". |
||
334 | * Maybe this should delete redirects at the target page of merges? |
||
335 | * |
||
336 | * @return bool Success |
||
337 | */ |
||
338 | function merge() { |
||
339 | # Get the titles directly from the IDs, in case the target page params |
||
340 | # were spoofed. The queries are done based on the IDs, so it's best to |
||
341 | # keep it consistent... |
||
342 | $targetTitle = Title::newFromID( $this->mTargetID ); |
||
343 | $destTitle = Title::newFromID( $this->mDestID ); |
||
344 | if ( is_null( $targetTitle ) || is_null( $destTitle ) ) { |
||
345 | return false; // validate these |
||
346 | } |
||
347 | if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) { |
||
348 | return false; |
||
349 | } |
||
350 | |||
351 | // MergeHistory object |
||
352 | $mh = new MergeHistory( $targetTitle, $destTitle, $this->mTimestamp ); |
||
353 | |||
354 | // Merge! |
||
355 | $mergeStatus = $mh->merge( $this->getUser(), $this->mComment ); |
||
356 | if ( !$mergeStatus->isOK() ) { |
||
357 | // Failed merge |
||
358 | $this->getOutput()->addWikiMsg( $mergeStatus->getMessage() ); |
||
359 | return false; |
||
360 | } |
||
361 | |||
362 | $targetLink = Linker::link( |
||
363 | $targetTitle, |
||
364 | null, |
||
365 | [], |
||
366 | [ 'redirect' => 'no' ] |
||
367 | ); |
||
368 | |||
369 | $this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' ) |
||
370 | ->rawParams( $targetLink ) |
||
371 | ->params( $destTitle->getPrefixedText() ) |
||
372 | ->numParams( $mh->getMergedRevisionCount() ) |
||
373 | ); |
||
374 | |||
375 | return true; |
||
376 | } |
||
377 | |||
378 | protected function getGroupName() { |
||
379 | return 'pagetools'; |
||
380 | } |
||
381 | } |
||
382 |
This check looks for type mismatches where the missing type is
false
. This is usually indicative of an error condtion.Consider the follow example
This function either returns a new
DateTime
object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returnedfalse
before passing on the value to another function or method that may not be able to handle afalse
.