Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 168
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionReviewFormUI
0.00% covered (danger)
0.00%
0 / 168
0.00% covered (danger)
0.00%
0 / 12
2550
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setDiffPriorRevRecord
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTopNotice
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setBottomNotice
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setIncludeVersions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHtml
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
240
 rejectRefRevId
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 ratingInputs
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
72
 getTagMsg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTagValueMsg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getRatingFormLevels
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 submitButtons
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2
3use MediaWiki\CommentStore\CommentStore;
4use MediaWiki\Html\Html;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Request\WebRequest;
7use MediaWiki\Revision\RevisionRecord;
8use MediaWiki\SpecialPage\SpecialPage;
9use MediaWiki\User\User;
10
11/**
12 * Main review form UI
13 *
14 * NOTE: use ONLY for diff-to-stable views and page version views
15 */
16class RevisionReviewFormUI {
17    private User $user;
18    private FlaggableWikiPage $article;
19    /** A notice inside the review box at the top (HTML) */
20    private string $topNotice = '';
21    /** A notice inside the review box at the bottom (HTML) */
22    private string $bottomNotice = '';
23    /** @var array<int,array<string,int>>|null */
24    private ?array $templateIds = null;
25    private WebRequest $request;
26    private RevisionRecord $revRecord;
27    private ?RevisionRecord $refRevRecord = null;
28
29    /**
30     * Generates a brief review form for a page
31     */
32    public function __construct(
33        IContextSource $context,
34        FlaggableWikiPage $article,
35        RevisionRecord $revRecord
36    ) {
37        $this->user = $context->getUser();
38        $this->request = $context->getRequest();
39        $this->article = $article;
40        $this->revRecord = $revRecord;
41    }
42
43    /**
44     * Call this only when the form is shown on a diff:
45     * (a) Shows the "reject" button
46     * (b) Default the rating tags to those of $this->revRecord (if flagged)
47     * @param RevisionRecord $refRevRecord Old revision for diffs ($this->revRecord is the new rev)
48     */
49    public function setDiffPriorRevRecord( RevisionRecord $refRevRecord ): void {
50        $this->refRevRecord = $refRevRecord;
51    }
52
53    /**
54     * Add on a notice inside the review box at the top
55     * @param string $notice HTML to show
56     */
57    public function setTopNotice( string $notice ): void {
58        $this->topNotice = $notice;
59    }
60
61    /**
62     * Add on a notice inside the review box at the bottom
63     * @param string $notice HTML to show
64     */
65    public function setBottomNotice( string $notice ): void {
66        $this->bottomNotice = $notice;
67    }
68
69    /**
70     * Set the template version parameters of what the user is viewing
71     * @param array<int,array<string,int>> $templateIds
72     */
73    public function setIncludeVersions( array $templateIds ): void {
74        $this->templateIds = $templateIds;
75    }
76
77    /**
78     * Generates a brief review form for a page
79     * @return array (html string, error string or true)
80     */
81    public function getHtml(): array {
82        $revId = $this->revRecord->getId();
83        if ( $this->revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
84            return [ '', 'review_bad_oldid' ]; # The revision must be valid and public
85        }
86        $article = $this->article; // convenience
87
88        $srev = $article->getStableRev();
89        # See if the version being displayed is flagged...
90        if ( $revId == $article->getStable() ) {
91            $frev = $srev; // avoid query
92        } else {
93            $frev = FlaggedRevision::newFromTitle( $article->getTitle(), $revId );
94        }
95        $oldTag = $frev ? $frev->getTag() : FlaggedRevs::quickTag();
96        $reviewTime = $frev ? $frev->getTimestamp() : ''; // last review of rev
97
98        $priorRevId = $this->refRevRecord ? $this->refRevRecord->getId() : 0;
99        # If we are reviewing updates to a page, start off with the stable revision's
100        # tag. Otherwise, we just fill them in with the selected revision's tag.
101        # @TODO: do we want to carry over info for other diffs?
102        if ( $srev && $srev->getRevId() == $priorRevId ) { // diff-to-stable
103            $tag = $srev->getTag();
104            # Check if user is allowed to renew the stable version.
105            # If not, then get the tag for the new revision itself.
106            if ( !FlaggedRevs::userCanSetTag( $this->user, $oldTag ) ) {
107                $tag = $oldTag;
108            }
109            # Re-review button is need for template only review case
110            $reviewIncludes = ( $srev->getRevId() == $revId && !$article->stableVersionIsSynced() );
111        } else { // views
112            $tag = $oldTag;
113            $reviewIncludes = false; // re-review button not needed
114        }
115
116        # Disable form for unprivileged users
117        $disabled = !MediaWikiServices::getInstance()->getPermissionManager()
118                ->quickUserCan( 'review', $this->user, $article->getTitle() ) ||
119            !FlaggedRevs::userCanSetTag( $this->user, $tag );
120
121        # Begin form...
122        $reviewTitle = SpecialPage::getTitleFor( 'RevisionReview' );
123        $action = $reviewTitle->getLocalURL( 'action=submit' );
124        $params = [ 'method' => 'post', 'action' => $action, 'id' => 'mw-fr-reviewform' ];
125        $form = Html::openElement( 'form', $params ) . "\n";
126        $form .= Html::openElement( 'fieldset',
127            [ 'class' => 'flaggedrevs_reviewform noprint cdx-card', 'style' => 'font-size: 90%;' ] ) . "\n";
128        # Add appropriate legend text
129        $legendMsg = $frev ? 'revreview-reflag' : 'revreview-flag';
130        $form .= Html::openElement( 'div', [ 'id' => 'mw-fr-reviewformlegend' ] );
131        $form .= '<div class="cdx-card__text__title">' . wfMessage( $legendMsg )->escaped() . '</div>';
132        # Show explanatory text
133        $form .= $this->topNotice;
134
135        # Start rating controls
136        $css = $disabled ? 'fr-rating-controls-disabled' : 'fr-rating-controls';
137        $form .= Html::openElement( 'p', [ 'class' => $css, 'id' => 'fr-rating-controls' ] ) . "\n";
138
139        # Add main checkboxes/selects
140        [ $radios, $radiosShown ] = $this->ratingInputs( $this->user, $tag, $disabled );
141        $form .= Html::rawElement(
142            'span',
143            [ 'id' => 'mw-fr-ratingselects', 'class' => 'fr-rating-options' ],
144            $radios
145        );
146
147        # Hide comment input if needed
148        if ( !$disabled ) {
149            $form .= '<div class="cdx-text-input" style="padding-bottom: 5px;">';
150            $form .= Xml::label( wfMessage( 'revreview-log' )->text(), 'mw-fr-commentbox' );
151            $form .= Xml::input(
152                'wpReason', 40, '',
153                [
154                    'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
155                    'class' => 'fr-comment-box cdx-text-input__input',
156                    'id' => 'mw-fr-commentbox'
157                ]
158            );
159            $form .= '</div>';
160        }
161
162        # Add the submit buttons...
163        $rejectId = $this->rejectRefRevId(); // determine if there will be reject button
164        $form .= $this->submitButtons( $rejectId, $frev, $disabled, $reviewIncludes, $radiosShown );
165
166        # Show stability log if there is anything interesting...
167        if ( $article->isPageLocked() ) {
168            $form .= FlaggedRevsXML::stabilityLogExcerpt( $article->getTitle() );
169        }
170
171        # End rating controls
172        $form .= Html::closeElement( 'p' ) . "\n";
173
174        # Show explanatory text
175        $form .= $this->bottomNotice;
176
177        # Hidden params
178        $form .= Html::hidden( 'title', $reviewTitle->getPrefixedText() ) . "\n";
179        $form .= Html::hidden( 'target', $article->getTitle()->getPrefixedDBkey() ) . "\n";
180        $form .= Html::hidden( 'refid', $priorRevId, [ 'id' => 'mw-fr-input-refid' ] ) . "\n";
181        $form .= Html::hidden( 'oldid', $revId, [ 'id' => 'mw-fr-input-oldid' ] ) . "\n";
182        $form .= Html::hidden( 'wpEditToken', $this->user->getEditToken() ) . "\n";
183        $form .= Html::hidden( 'changetime', $reviewTime,
184            [ 'id' => 'mw-fr-input-changetime' ] ) . "\n"; // id for JS
185        # Special token to discourage fiddling...
186        $key = $this->request->getSessionData( 'wsFlaggedRevsKey' );
187        $checkCode = RevisionReviewForm::validationKey( $revId, $key );
188        $form .= Html::hidden( 'validatedParams', $checkCode ) . "\n";
189        $form .= Html::closeElement( 'fieldset' ) . "\n";
190        $form .= Html::closeElement( 'form' ) . "\n";
191        return [ $form, true /* ok */ ];
192    }
193
194    /**
195     * If the REJECT button should show then get the ID of the last good rev
196     */
197    private function rejectRefRevId(): int {
198        if ( $this->refRevRecord ) {
199            $priorId = $this->refRevRecord->getId();
200            if ( $priorId == $this->article->getStable() &&
201                $priorId != $this->revRecord->getId() &&
202                !$this->revRecord->hasSameContent( $this->refRevRecord )
203            ) {
204                return $priorId; // left rev must be stable and right one newer
205            }
206        }
207        return 0;
208    }
209
210    /**
211     * Generate main tag radio buttons for review form if necessary
212     * @param User $user
213     * @param int|null $selected selected tag
214     * @param bool $disabled form disabled
215     * @return array{0:string,1:bool} The tags HTML and whether it contains radio buttons
216     */
217    private function ratingInputs( User $user, ?int $selected, bool $disabled ): array {
218        if ( FlaggedRevs::binaryFlagging() ) {
219            return [ '', false ];
220        }
221
222        $quality = FlaggedRevs::getTagName();
223        $levels = $this->getRatingFormLevels( $user );
224        if ( $disabled || count( $levels ) === 0 ) {
225            // Display the value for the tag as text
226            return [
227                $this->getTagMsg( $quality )->escaped() . ': ' . $this->getTagValueMsg( $selected ?? 0 ),
228                false
229            ];
230        }
231
232        $minLevel = array_keys( $levels )[ 0 ];
233        $inputName = "wp$quality";
234        if ( count( $levels ) === 1 ) {
235            // No need to display the single settable level
236            return [
237                Html::hidden( $inputName, $minLevel ),
238                false
239            ];
240        }
241
242        # Determine the level selected by default
243        if ( !$selected || !isset( $levels[$selected] ) ) {
244            $selected = $minLevel;
245        }
246
247        $inputs = $this->getTagMsg( $quality )->escaped() . ":\n";
248        foreach ( $levels as $i => $name ) {
249            $id = $inputName . $i;
250            $item = Html::radio(
251                $inputName,
252                $i == $selected,
253                [ 'value' => $i, 'id' => $id, 'class' => "fr-rating-option-$i cdx-radio__input" ]
254            );
255            $item .= "\u{00A0}";
256            $item .= '<span class="cdx-radio__icon"></span>';
257            $item .= Html::label(
258                $this->getTagMsg( $name )->text(),
259                $id,
260                [ 'class' => "fr-rating-option-$i cdx-radio__label" ]
261            );
262            $inputs .= Html::rawElement( 'span', [ 'class' => 'cdx-radio cdx-radio--inline' ], $item );
263        }
264        return [ $inputs, true ];
265    }
266
267    /**
268     * Get the UI name for a tag
269     */
270    private function getTagMsg( string $tag ): Message {
271        return wfMessage( "revreview-$tag" );
272    }
273
274    /**
275     * Get the UI name for a value of a tag
276     */
277    private function getTagValueMsg( int $value ): string {
278        $quality = FlaggedRevs::getTagName();
279        // Possible message keys that come pre-defined with the extension:
280        // * revreview-accuracy-0
281        // * revreview-accuracy-1
282        // * revreview-accuracy-2
283        // * revreview-accuracy-3
284        // * revreview-accuracy-4
285        $msg = wfMessage( "revreview-$quality-$value" );
286        return $msg->isDisabled() ? (string)$value : $msg->escaped();
287    }
288
289    /**
290     * Get all available tags for this user
291     * @return array<int,string>
292     */
293    private function getRatingFormLevels( User $user ): array {
294        $quality = FlaggedRevs::getTagName();
295        $labels = []; // applicable tag levels
296        for ( $i = 1; $i <= FlaggedRevs::getMaxLevel(); $i++ ) {
297            # Some levels may be restricted or not applicable...
298            if ( FlaggedRevs::userCanSetValue( $user, $i ) ) {
299                $labels[$i] = "$quality-$i";
300            }
301        }
302        return $labels;
303    }
304
305    /**
306     * Generates review form submit buttons
307     * @param int $rejectId left rev ID for "reject" on diffs
308     * @param FlaggedRevision|null $frev the flagged revision, if any
309     * @param bool $disabled is the form disabled?
310     * @param bool $reviewIncludes force the review button to be usable?
311     * @param bool $radiosShown Whether any radio buttons appear. If not,
312     *  and there are no pending changes (either on the page itself or in
313     *  transcluded templates), the review button will be disabled.
314     */
315    private function submitButtons(
316        int $rejectId, ?FlaggedRevision $frev, bool $disabled, bool $reviewIncludes, bool $radiosShown
317    ): string {
318        $disAttrib = [ 'disabled' => 'disabled' ];
319        # ACCEPT BUTTON: accept a revision
320        # We may want to re-review to change:
321        # (a) notes (b) tags (c) pending template changes
322        if ( !$radiosShown ) { // just the buttons
323            $applicable = ( !$frev || $reviewIncludes ); // no tags/notes
324            $needsChange = false; // no state change possible
325        } else { // buttons + ratings
326            $applicable = true; // tags might change
327            $needsChange = ( $frev && !$reviewIncludes );
328        }
329        // Disable buttons unless state changes in some cases (non-JS compatible)
330        $needsChangeAttrib = $needsChange ? [ 'data-mw-fr-review-needs-change' => '' ] : [];
331        $s = Xml::submitButton( wfMessage( 'revreview-submit-review' )->text(),
332            [
333                'name'      => 'wpApprove',
334                'id'        => 'mw-fr-submit-accept',
335                'class' => 'cdx-button cdx-button--action-progressive',
336                'accesskey' => wfMessage( 'revreview-ak-review' )->text(),
337                'title'     => wfMessage( 'revreview-tt-flag' )->text() . ' [' .
338                    wfMessage( 'revreview-ak-review' )->text() . ']'
339            ] + ( ( $disabled || !$applicable ) ? $disAttrib : [] ) + $needsChangeAttrib
340        );
341        # REJECT BUTTON: revert from a pending revision to the stable
342        if ( $rejectId ) {
343            $s .= ' ';
344            $s .= Xml::submitButton( wfMessage( 'revreview-submit-reject' )->text(),
345                [
346                    'name'  => 'wpReject',
347                    'id'    => 'mw-fr-submit-reject',
348                    'class' => 'cdx-button cdx-button--action-destructive',
349                    'title' => wfMessage( 'revreview-tt-reject' )->text(),
350                ] + ( $disabled ? $disAttrib : [] )
351            );
352        }
353        # UNACCEPT BUTTON: revoke a revisions acceptance
354        # Hide if revision is not flagged
355        $s .= ' ';
356        $s .= Xml::submitButton( wfMessage( 'revreview-submit-unreview' )->text(),
357            [
358                'name'  => 'wpUnapprove',
359                'id'    => 'mw-fr-submit-unaccept',
360                'class' => 'cdx-button cdx-button--action-destructive',
361                'title' => wfMessage( 'revreview-tt-unflag' )->text(),
362                'style' => $frev ? '' : 'display:none'
363            ] + ( $disabled ? $disAttrib : [] )
364        ) . "\n";
365        return $s;
366    }
367}