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\Html\Html; |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\Request\WebRequest; |
7 | use MediaWiki\Revision\RevisionRecord; |
8 | use MediaWiki\SpecialPage\SpecialPage; |
9 | use 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 | */ |
16 | class 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 | } |