Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionReview
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 7
3422
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 1
812
 approvalSuccessHTML
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 deapprovalSuccessHTML
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialLinks
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 doReview
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
552
1<?php
2
3use MediaWiki\Context\RequestContext;
4use MediaWiki\Exception\PermissionsError;
5use MediaWiki\Exception\UserBlockedError;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Permissions\PermissionManager;
8use MediaWiki\Session\CsrfTokenSet;
9use MediaWiki\SpecialPage\SpecialPage;
10use MediaWiki\SpecialPage\UnlistedSpecialPage;
11use MediaWiki\Title\Title;
12
13class RevisionReview extends UnlistedSpecialPage {
14    /** @var RevisionReviewForm|null */
15    private $form;
16    /** @var Title|null */
17    private $title;
18
19    public function __construct(
20        private readonly PermissionManager $permissionManager,
21    ) {
22        parent::__construct( 'RevisionReview', 'review' );
23    }
24
25    /**
26     * @inheritDoc
27     */
28    public function doesWrites() {
29        return true;
30    }
31
32    /**
33     * @inheritDoc
34     */
35    public function execute( $par ) {
36        $out = $this->getOutput();
37        $user = $this->getUser();
38        $request = $this->getRequest();
39
40        # Our target page
41        $this->title = Title::newFromText( $request->getVal( 'target' ) );
42        if ( !$this->title ) {
43            $out->showErrorPage( 'notargettitle', 'notargettext' );
44            return;
45        }
46
47        if ( !$this->permissionManager->userHasRight( $user, 'review' ) ) {
48            throw new PermissionsError( 'review' );
49        }
50
51        $confirmed = $user->matchEditToken( $request->getVal( 'wpEditToken' ) );
52        if ( $this->permissionManager->isBlockedFrom( $user, $this->title, !$confirmed ) ) {
53            // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Guaranteed via isBlockedFrom() above
54            throw new UserBlockedError( $user->getBlock( !$confirmed ) );
55        }
56
57        $this->checkReadOnly();
58        $this->setHeaders();
59
60        # Basic page permission checks...
61        $permStatus = $this->permissionManager->getPermissionStatus(
62            'review',
63            $user,
64            $this->title,
65            PermissionManager::RIGOR_QUICK
66        );
67        if ( !$permStatus->isGood() ) {
68            $out->showPermissionStatus( $permStatus, 'review' );
69            return;
70        }
71
72        $form = new RevisionReviewForm( $user );
73        $this->form = $form;
74
75        $form->setTitle( $this->title );
76        # Param for sites with binary flagging
77        if ( $request->getCheck( 'wpApprove' ) ) {
78            $form->setAction( RevisionReviewForm::ACTION_APPROVE );
79        } elseif ( $request->getCheck( 'wpUnapprove' ) ) {
80            $form->setAction( RevisionReviewForm::ACTION_UNAPPROVE );
81        } elseif ( $request->getCheck( 'wpReject' ) ) {
82            $form->setAction( RevisionReviewForm::ACTION_REJECT );
83        }
84        # Rev ID
85        $form->setOldId( $request->getInt( 'oldid' ) );
86        $form->setRefId( $request->getInt( 'refid' ) );
87        # Special parameter mapping
88        $form->setTemplateParams( $request->getVal( 'templateParams' ) );
89        # Special token to discourage fiddling...
90        $form->setValidatedParams( $request->getVal( 'validatedParams' ) );
91        # Conflict handling
92        $form->setLastChangeTime( $request->getVal( 'changetime' ) );
93        # Session key
94        $form->setSessionKey( $request->getSessionData( 'wsFlaggedRevsKey' ) );
95        # Tag values
96        # This can be NULL if we uncheck a checkbox
97        $form->setTag( $request->getInt( 'wp' . FlaggedRevs::getTagName() ) );
98        # Log comment
99        $form->setComment( $request->getText( 'wpReason' ) );
100        $form->ready();
101
102        if ( !$request->wasPosted() ) {
103            // No form to view (GET)
104            $out->returnToMain( false, $this->title );
105            return;
106        }
107        // Review the edit if requested (POST)...
108
109        // Check the edit token...
110        if ( !$confirmed ) {
111            $out->addWikiMsg( 'sessionfailure' );
112            $out->returnToMain( false, $this->title );
113            return;
114        }
115
116        // Use confirmation screen for reject...
117        if ( $form->getAction() == RevisionReviewForm::ACTION_REJECT && !$request->getBool( 'wpRejectConfirm' ) ) {
118            $rejectForm = new RejectConfirmationFormUI( $form );
119            [ $html, $status ] = $rejectForm->getHtml();
120            if ( $status === true ) {
121                // Success...
122                $out->addHTML( $html );
123            } else {
124                // Failure...
125                if ( $status === 'review_page_unreviewable' ) {
126                    $out->addWikiMsg( 'revreview-main' );
127                    return;
128                } elseif ( $status === 'review_page_notexists' ) {
129                    $out->showErrorPage( 'internalerror', 'nopagetext' );
130                    return;
131                } elseif ( $status === 'review_bad_oldid' ) {
132                    $out->showErrorPage( 'internalerror', 'revreview-revnotfound' );
133                } else {
134                    $out->showErrorPage( 'internalerror', $status );
135                }
136                $out->returnToMain( false, $this->title );
137            }
138            return;
139        }
140
141        // Otherwise submit...
142        $status = $form->submit();
143        if ( $status === true ) {
144            // Success...
145            $out->setPageTitleMsg( $this->msg( 'actioncomplete' ) );
146            if ( $form->getAction() === RevisionReviewForm::ACTION_APPROVE ) {
147                $out->addHTML( $this->approvalSuccessHTML() );
148            } elseif ( $form->getAction() === RevisionReviewForm::ACTION_UNAPPROVE ) {
149                $out->addHTML( $this->deapprovalSuccessHTML() );
150            } elseif ( $form->getAction() === RevisionReviewForm::ACTION_REJECT ) {
151                $query = $this->title->isRedirect() ? [ 'redirect' => 'no' ] : [];
152                $out->redirect( $this->title->getFullURL( $query ) );
153            }
154        } else {
155            // Failure...
156            if ( $status === 'review_page_unreviewable' ) {
157                $out->addWikiMsg( 'revreview-main' );
158                return;
159            } elseif ( $status === 'review_page_notexists' ) {
160                $out->showErrorPage( 'internalerror', 'nopagetext' );
161                return;
162            } elseif ( $status === 'review_denied' || $status === 'review_bad_key' ) {
163                throw new PermissionsError( 'badaccess-group0' );
164            } elseif ( $status === 'review_bad_oldid' ) {
165                $out->showErrorPage( 'internalerror', 'revreview-revnotfound' );
166            } elseif ( $status === 'review_not_flagged' ) {
167                $out->redirect( $this->title->getFullURL() ); // already unflagged
168            } elseif ( $status === 'review_too_low' ) {
169                $out->addWikiMsg( 'revreview-toolow' );
170            } else {
171                $out->showErrorPage( 'internalerror', $status );
172            }
173            $out->returnToMain( false, $this->title );
174        }
175    }
176
177    /**
178     * @return string HTML
179     */
180    private function approvalSuccessHTML() {
181        $title = $this->form->getTitle();
182        # Show success message
183        $s = "<div class='plainlinks'>";
184        $s .= $this->msg( 'revreview-successful',
185            $title->getPrefixedText(), $title->getPrefixedURL() )->parseAsBlock();
186        $s .= $this->msg( 'revreview-stable1',
187            $title->getPrefixedURL(), $this->form->getOldId() )->parseAsBlock();
188        $s .= "</div>";
189        # Handy links to special pages
190        if ( $this->permissionManager->userHasRight( $this->getUser(), 'unreviewedpages' ) ) {
191            $s .= $this->getSpecialLinks();
192        }
193        return $s;
194    }
195
196    /**
197     * @return string HTML
198     */
199    private function deapprovalSuccessHTML() {
200        $title = $this->form->getTitle();
201        # Show success message
202        $s = "<div class='plainlinks'>";
203        $s .= $this->msg( 'revreview-successful2',
204            $title->getPrefixedText(), $title->getPrefixedURL() )->parseAsBlock();
205        $s .= $this->msg( 'revreview-stable2',
206            $title->getPrefixedURL(), $this->form->getOldId() )->parseAsBlock();
207        $s .= "</div>";
208        # Handy links to special pages
209        if ( $this->permissionManager->userHasRight( $this->getUser(), 'unreviewedpages' ) ) {
210            $s .= $this->getSpecialLinks();
211        }
212        return $s;
213    }
214
215    /**
216     * @return string HTML
217     */
218    private function getSpecialLinks() {
219        $linkRenderer = $this->getLinkRenderer();
220        $s = '<p>' . $this->msg( 'returnto' )->rawParams(
221            $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'UnreviewedPages' ) )
222        )->escaped() . '</p>';
223        $s .= '<p>' . $this->msg( 'returnto' )->rawParams(
224            $linkRenderer->makeKnownLink( SpecialPage::getTitleFor( 'PendingChanges' ) )
225        )->escaped() . '</p>';
226        return $s;
227    }
228
229    /**
230     * @param array $argsMap Typical params are oldid, refid, validatedParams,
231     * templateParams, wpApprove, wpUnapprove, wpReject, wpReason, changetime, wpEditToken,
232     * wpaccuracy, and target.
233     * @return array
234     */
235    public static function doReview( $argsMap ) {
236        $context = RequestContext::getMain();
237        $user = $context->getUser();
238        $out = $context->getOutput();
239        $request = $context->getRequest();
240        if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
241            return [ 'error-html' => wfMessage( 'revreview-failed' )->parse() .
242                wfMessage( 'revreview-submission-invalid' )->parse() ];
243        }
244        // Make review interface object
245        $form = new RevisionReviewForm( $user );
246        $title = null; // target page
247        $editToken = ''; // edit token
248
249        foreach ( $argsMap as $par => $val ) {
250            switch ( $par ) {
251                case "target":
252                    $title = Title::newFromURL( $val );
253                    break;
254                case "oldid":
255                    $form->setOldId( $val );
256                    break;
257                case "refid":
258                    $form->setRefId( $val );
259                    break;
260                case "validatedParams":
261                    $form->setValidatedParams( $val );
262                    break;
263                case "templateParams":
264                    $form->setTemplateParams( $val );
265                    break;
266                case "wpApprove":
267                    if ( $val ) {
268                        $form->setAction( RevisionReviewForm::ACTION_APPROVE );
269                    }
270                    break;
271                case "wpUnapprove":
272                    if ( $val ) {
273                        $form->setAction( RevisionReviewForm::ACTION_UNAPPROVE );
274                    }
275                    break;
276                case "wpReject":
277                    if ( $val ) {
278                        $form->setAction( RevisionReviewForm::ACTION_REJECT );
279                    }
280                    break;
281                case "wpReason":
282                    $form->setComment( $val ?? '' );
283                    break;
284                case "changetime":
285                    $form->setLastChangeTime( $val );
286                    break;
287                case "wpEditToken":
288                    $editToken = $val;
289                    break;
290                case 'wp' . FlaggedRevs::getTagName():
291                    $form->setTag( $val );
292                    break;
293            }
294        }
295
296        # Valid target title?
297        if ( !$title ) {
298            return [ 'error-html' => wfMessage( 'notargettext' )->parse() ];
299        }
300
301        $form->setTitle( $title );
302        $form->setSessionKey( $request->getSessionData( 'wsFlaggedRevsKey' ) );
303
304        $form->ready(); // all params loaded
305        # Check session via user token
306        $userToken = new CsrfTokenSet( $request );
307        if ( !$userToken->matchToken( $editToken ) ) {
308            return [ 'error-html' => wfMessage( 'sessionfailure' )->parse() ];
309        }
310        # Basic permission checks...
311        $permStatus = MediaWikiServices::getInstance()->getPermissionManager()
312            ->getPermissionStatus( 'review', $user, $title, PermissionManager::RIGOR_QUICK );
313        if ( !$permStatus->isGood() ) {
314            return [ 'error-html' => $out->parseAsInterface(
315                $out->formatPermissionStatus( $permStatus, 'review' )
316            ) ];
317        }
318        # Try submission...
319        $status = $form->submit();
320        # Failure...
321        if ( $status !== true ) {
322            return [ 'error-html' => wfMessage( 'revreview-failed' )->parseAsBlock() .
323                '<p>' . wfMessage( $status )->escaped() . '</p>' ];
324        } elseif ( !$form->getAction() ) {
325            return [ 'error-html' => wfMessage( 'revreview-failed' )->parse() ];
326        }
327
328        # Sent new lastChangeTime TS to client for later submissions...
329        return [ 'change-time' => $form->getNewLastChangeTime() ];
330    }
331}