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