Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 257
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggedRevsHTML
0.00% covered (danger)
0.00%
0 / 257
0.00% covered (danger)
0.00%
0 / 9
1560
0.00% covered (danger)
0.00%
0 / 1
 getNamespaceMenu
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 getDefaultFilterMenu
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 getRestrictionFilterMenu
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 addTagRatings
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
6
 getEditTagFilterMenu
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 reviewDialog
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 1
380
 addMessageBox
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 diffToggle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 stabilityLogExcerpt
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3use MediaWiki\Context\IContextSource;
4use MediaWiki\Html\Html;
5use MediaWiki\Logging\LogEventsList;
6use MediaWiki\MediaWikiServices;
7use MediaWiki\Title\Title;
8
9/**
10 * Class containing utility HTML functions for a FlaggedRevs.
11 * Includes functions for selectors, icons, notices, CSS, and form aspects.
12 */
13class FlaggedRevsHTML {
14
15    /**
16     * Get a selector of reviewable namespaces
17     * @param int|null $selected namespace selected
18     * @param string|null $all Value of an item denoting all namespaces, or null to omit
19     */
20    public static function getNamespaceMenu( ?int $selected = null, ?string $all = null ): string {
21        $s = Html::rawElement( 'div', [ 'class' => 'cdx-field__item' ],
22            Html::rawElement( 'div', [ 'class' => 'cdx-label' ],
23                Html::label(
24                    wfMessage( 'namespace' )->text(),
25                    'namespace',
26                    [ 'class' => 'cdx-label__label' ]
27                )
28            )
29        );
30
31        # No namespace selected; let exact match work without hitting Main
32        $selected ??= '';
33        $s .= "\n<select id='namespace' name='namespace' class='cdx-select namespaceselector'>\n";
34        $arr = MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces();
35        if ( $all !== null ) {
36            $arr = [ $all => wfMessage( 'namespacesall' )->text() ] + $arr; // should be first
37        }
38        foreach ( $arr as $index => $name ) {
39            # Content pages only (except 'all')
40            if ( $index !== $all && !FlaggedRevs::isReviewNamespace( $index ) ) {
41                continue;
42            }
43            $name = $index !== 0 ? $name : wfMessage( 'blanknamespace' )->text();
44            if ( $index === $selected ) {
45                $s .= "\t" . Html::element( 'option', [ 'value' => $index,
46                        "selected" => "selected" ], $name ) . "\n";
47            } else {
48                $s .= "\t" . Html::element( 'option', [ 'value' => $index ], $name ) . "\n";
49            }
50        }
51        $s .= "</select>\n";
52        return $s;
53    }
54
55    /**
56     * Get a <select> of default page version (stable or draft). Used for filters.
57     * @param int|null $selected (0=draft, 1=stable, null=either )
58     */
59    public static function getDefaultFilterMenu( ?int $selected = null ): string {
60        $s = Html::rawElement( 'div', [ 'class' => 'cdx-field__item' ],
61            Html::rawElement( 'div', [ 'class' => 'cdx-label' ],
62                Html::label(
63                    wfMessage( 'revreview-defaultfilter' )->text(),
64                    'wpStable',
65                    [ 'class' => 'cdx-label__label' ]
66                )
67            )
68        );
69
70        $selectOptions = Html::element( 'option', [ 'value' => '', 'selected' => $selected === null ],
71            wfMessage( 'revreview-def-all' )->text() );
72        $selectOptions .= Html::element( 'option', [ 'value' => '1', 'selected' => $selected === 1 ],
73            wfMessage( 'revreview-def-stable' )->text() );
74        $selectOptions .= Html::element( 'option', [ 'value' => '0', 'selected' => $selected === 0 ],
75            wfMessage( 'revreview-def-draft' )->text() );
76
77        $s .= Html::rawElement( 'select', [
78            'name' => 'stable',
79            'id' => 'wpStable',
80            'class' => 'cdx-select filterselector'
81        ], $selectOptions );
82
83        return $s;
84    }
85
86    /**
87     * Get a <select> of options of 'autoreview' restriction levels. Used for filters.
88     * @param string|null $selected (null or empty string for "any", 'none' for none)
89     */
90    public static function getRestrictionFilterMenu( ?string $selected = '' ): string {
91        $s = Html::rawElement( 'div', [ 'class' => 'cdx-field__item' ],
92            Html::rawElement( 'div', [ 'class' => 'cdx-label' ],
93                Html::label(
94                    wfMessage( 'revreview-restrictfilter' )->text(),
95                    'wpRestriction',
96                    [ 'class' => 'cdx-label__label' ]
97                )
98            )
99        );
100
101        $selectOptions = Html::element( 'option',
102            [ 'value' => '', 'selected' => ( $selected ?? '' ) === '' ],
103            wfMessage( 'revreview-restriction-any' )->text()
104        );
105
106        if ( !FlaggedRevs::useProtectionLevels() ) {
107            # All "protected" pages have a protection level, not "none"
108            $selectOptions .= Html::element( 'option',
109                [ 'value' => 'none', 'selected' => $selected === 'none' ],
110                wfMessage( 'revreview-restriction-none' )->text()
111            );
112        }
113
114        foreach ( FlaggedRevs::getRestrictionLevels() as $perm ) {
115            // Give grep a chance to find the usages:
116            // revreview-restriction-any, revreview-restriction-none
117            $key = "revreview-restriction-$perm";
118            $msg = wfMessage( $key )->isDisabled() ? $perm : wfMessage( $key )->text();
119            $selectOptions .= Html::element( 'option',
120                [ 'value' => $perm, 'selected' => $selected == $perm ],
121                $msg
122            );
123        }
124
125        $s .= Html::rawElement( 'select', [
126            'name' => 'restriction',
127            'id' => 'wpRestriction',
128            'class' => 'cdx-select restrictionselector'
129        ], $selectOptions );
130
131        return $s;
132    }
133
134    /**
135     * Generates a review box/tag displaying the quality level based on flags.
136     *
137     * This method creates a simple HTML table with two cells: one for the quality label
138     * and the other for the corresponding rating. The table is only generated if the page
139     * is not protected by FlaggedRevs settings.
140     *
141     * @param array $flags An associative array containing the flag ratings.
142     *
143     * @return string The generated HTML string for the review box/tag.
144     */
145    public static function addTagRatings( array $flags ): string {
146        if ( FlaggedRevs::useOnlyIfProtected() ) {
147            return '';
148        }
149
150        $quality = FlaggedRevs::getTagName();
151        $level = $flags[$quality] ?? 0;
152        $encValueText = wfMessage( "revreview-$quality-$level" )->text();
153        $levelClass = 'fr-value' . ( $level * 20 + 20 );
154
155        return Html::rawElement( 'table', [
156            'id' => 'mw-fr-revisionratings-box',
157            'class' => 'flaggedrevs-color-1',
158            'style' => 'margin: auto;',
159            'cellpadding' => '0',
160        ],
161            Html::rawElement( 'tr', [],
162                Html::element( 'td', [ 'class' => 'fr-text', 'style' => 'vertical-align: middle;' ],
163                    wfMessage( "revreview-$quality" )->text()
164                ) .
165                Html::element( 'td', [ 'class' => $levelClass, 'style' => 'vertical-align: middle;' ],
166                    $encValueText
167                )
168            )
169        );
170    }
171
172    /**
173     * Generates a dropdown menu for edit tag filters
174     *
175     * @param IContextSource $context
176     * @param string|null $selected (null or empty string for "any")
177     * @since 1.43
178     */
179    public static function getEditTagFilterMenu( IContextSource $context, ?string $selected = '' ): string {
180        $s = Html::rawElement( 'div', [ 'class' => 'cdx-field__item' ],
181            Html::rawElement( 'div', [ 'class' => 'cdx-label' ],
182                Html::label(
183                    $context->msg( 'pendingchanges-edit-tag' )->text(),
184                    'wpTagFilter',
185                    [ 'class' => 'cdx-label__label' ]
186                )
187            )
188        );
189
190        $selectOptions = Html::element( 'option',
191            [ 'value' => '', 'selected' => ( $selected ?? '' ) === '' ],
192            $context->msg( 'pendingchanges-edit-tag-any' )->text()
193        );
194
195        $tagDefs = MediaWikiServices::getInstance()->getChangeTagsFormatter()->getChangeTagList(
196            $context,
197            $context->getAuthority()
198        );
199        foreach ( $tagDefs as $tagInfo ) {
200            $tagName = $tagInfo['name'];
201            $selectOptions .= Html::element( 'option',
202                [ 'value' => $tagName, 'selected' => $selected == $tagName ],
203                $tagName
204            );
205        }
206
207        $s .= Html::rawElement( 'select', [
208            'name' => 'tagFilter',
209            'id' => 'wpTagFilter',
210            'class' => 'cdx-select'
211        ], $selectOptions );
212
213        return $s;
214    }
215
216    /**
217     * Generates a review box using a table using FlaggedRevsHTML::addTagRatings()
218     *
219     * @param IContextSource $context
220     * @param FlaggedRevision|null $frev the reviewed version
221     * @param int $revisionId the revision ID
222     * @param int $revsSince revisions since review
223     * @param string $type (stable/draft/oldstable)
224     * @param bool $synced does stable=current and this is one of them?
225     *
226     * @return string
227     */
228    public static function reviewDialog(
229        IContextSource $context,
230        ?FlaggedRevision $frev,
231        int $revisionId,
232        int $revsSince,
233        string $type = 'oldstable',
234        bool $synced = false
235    ): string {
236        $href = '';
237        $user = $context->getAuthority();
238        $skin = $context->getSkin();
239        $lang = $context->getLanguage();
240
241        // If $frev is null, show a dialog with a "no flagged revision" message
242        if ( $frev === null ) {
243            $subtitleMessageKey = 'revreview-unchecked-title';
244            $msg = 'revreview-noflagged';
245            $subtitle = $context->msg( $subtitleMessageKey )->text();
246            $html = $context->msg( $msg )->parse();
247        } else {
248            // Regular case when $frev is not null
249            $flags = $frev->getTags();
250            $time = $lang->date( $frev->getTimestamp(), true );
251
252            $subtitleMessageKey = ( $type === 'stable' || $synced )
253                ? 'revreview-basic-title' // This is a checked version of this page
254                : 'revreview-draft-title'; // Pending changes are displayed on this page
255
256            $subtitle = $context->msg( $subtitleMessageKey )->text();
257
258            // Construct some tagging
259            if ( $synced && ( $type == 'stable' || $type == 'draft' ) ) {
260                $msg = 'revreview-basic-same';
261                $html = $context->msg( $msg, $frev->getRevId(), $time )->numParams( $revsSince )->parse();
262            } elseif ( $type == 'oldstable' ) {
263                $msg = 'revreview-basic-old';
264                $html = $context->msg( $msg, $frev->getRevId(), $time )->parse();
265            } else {
266                $msg = $type === 'stable' ? 'revreview-basic' : 'revreview-newest-basic';
267                $msg .= !$revsSince ? '-i' : '';
268                $html = $context->msg( $msg, $frev->getRevId(), $time )->numParams( $revsSince )->parse();
269            }
270
271            // Add any rating tags as needed...
272            if ( $flags && !FlaggedRevs::binaryFlagging() ) {
273                if ( $skin->getSkinName() !== 'minerva' ) {
274                    // Don't show the ratings on draft views
275                    if ( $type == 'stable' || $type == 'oldstable' ) {
276                        $html .= '<p>' . self::addTagRatings( $flags ) . '</p>';
277                    }
278                }
279            }
280
281            $title = $frev->getTitle();
282            $href = $title->getFullURL( [ 'diff' => 'cur', 'oldid' => $revisionId ] );
283        }
284
285        if ( $skin && $skin->getSkinName() === 'minerva' ) {
286            return self::addMessageBox( 'inline', $html, [
287                'class' => 'mw-fr-mobile-message-inline',
288            ] );
289        } else {
290            return Html::rawElement(
291                'div',
292                [
293                    'id' => 'mw-fr-revision-details',
294                    'class' => 'mw-fr-revision-details-dialog',
295                    'style' => 'display:none;'
296                ],
297                Html::rawElement( 'div', [ 'tabindex' => '0' ] ) .
298                Html::rawElement(
299                    'div',
300                    [ 'class' => 'cdx-dialog cdx-dialog--horizontal-actions' ],
301                    Html::rawElement(
302                        'header',
303                        [ 'class' => 'cdx-dialog__header cdx-dialog__header--default' ],
304                        Html::rawElement(
305                            'div',
306                            [ 'class' => 'cdx-dialog__header__title-group' ],
307                            Html::element( 'h2', [ 'class' => 'cdx-dialog__header__title' ],
308                                $context->msg( 'revreview-dialog-title' )->text() ) .
309                            Html::element( 'p', [ 'class' => 'cdx-dialog__header__subtitle' ], $subtitle )
310                        ) .
311                        Html::rawElement( 'button', [
312                            'class' => 'cdx-button cdx-button--action-default cdx-button--weight-quiet
313                            cdx-button--size-medium cdx-button--icon-only cdx-dialog__header__close-button',
314                            'aria-label' => $context->msg( 'fr-revision-info-dialog-close-aria-label' )->text(),
315                            'onclick' => 'document.getElementById("mw-fr-revision-details").style.display = "none";'
316                        ],
317                            Html::rawElement( 'span', [ 'class' => 'cdx-icon cdx-icon--medium
318                            cdx-fr-css-icon--close' ] )
319                        )
320                    ) .
321                    Html::rawElement(
322                        'div',
323                        [ 'class' => 'cdx-dialog__body' ],
324                        $html
325                    ) .
326                    ( $frev !== null && $user->isAllowed( 'review' ) ?
327                        Html::rawElement(
328                            'footer',
329                            [ 'class' => 'cdx-dialog__footer cdx-dialog__footer--default' ],
330                            Html::rawElement( 'div', [ 'class' => 'cdx-dialog__footer__actions' ],
331                                Html::element(
332                                    'a',
333                                    [
334                                        'href' => $href,
335                                        'class' =>
336                                            'cdx-button cdx-button--action-progressive cdx-button--weight-primary
337                                            cdx-button--size-medium cdx-dialog__footer__primary-action
338                                            cdx-button--fake-button cdx-button--fake-button--enabled'
339                                    ],
340                                    $context->msg( 'fr-revision-info-dialog-review-button' )->text()
341                                ) .
342                                Html::element(
343                                    'button',
344                                    [
345                                        'class' => 'cdx-dialog__footer__default-action cdx-button cdx-button--default',
346                                        'onclick' =>
347                                            'document.getElementById("mw-fr-revision-details").style.display = "none";'
348                                    ],
349                                    $context->msg( 'fr-revision-info-dialog-cancel-button' )->text()
350                                )
351                            )
352                        ) : '' )
353                ) .
354                Html::rawElement( 'div', [ 'tabindex' => '0' ] )
355            );
356        }
357    }
358
359    /**
360     * Generates a custom message box using the `cdx-message` class.
361     *
362     * This method creates a message box with the `cdx-message` class, which can be configured
363     * as either `inline` or `block`. The method allows for additional attributes to be passed
364     * to further customize the appearance and behavior of the message box.
365     *
366     * The message box will include:
367     * - An outer `div` element with the appropriate `cdx-message` classes and any additional
368     *   classes or attributes specified in the `$attrs` parameter.
369     * - A `span` element for the message icon, using the `cdx-message__icon` class.
370     * - A nested `div` element to contain the message content, using the `cdx-message__content` class.
371     *
372     * This method is useful for creating consistent, styled message boxes across the application.
373     *
374     * @param string $type The type of message box to create, either 'inline' or 'block'.
375     *                     This determines the overall structure and style of the message box.
376     * @param string $message The content to display inside the message box. This can include
377     *                        HTML or plain text, depending on the context.
378     * @param array $attrs Optional. An associative array of additional HTML attributes to
379     *                     apply to the outer `div` element of the message box. The `class`
380     *                     attribute can be extended by passing additional classes in this array.
381     * @return string The generated HTML string for the complete message box, ready to be
382     *                rendered in the output.
383     */
384    public static function addMessageBox( string $type, string $message, array $attrs = [] ): string {
385        // Base classes for the message box, including type and notice class
386        $baseClass = 'cdx-message mw-fr-message-box cdx-message--' . $type . ' cdx-message--notice';
387
388        // Merge custom attributes with the default class
389        $attrs['class'] = isset( $attrs['class'] ) ? $baseClass . ' ' . $attrs['class'] : $baseClass;
390
391        // Generate and return the complete HTML for the message box
392        return Html::rawElement(
393            'div',
394            $attrs,
395            Html::element( 'span', [ 'class' => 'cdx-message__icon' ] ) .
396            Html::rawElement( 'div', [ 'class' => 'cdx-message__content' ], $message )
397        );
398    }
399
400    /**
401     * Generates the "(show/hide)" diff toggle. With JS disabled, it functions as a link to the diff.
402     *
403     * @param Title $title
404     * @param int $fromrev
405     * @param int $torev
406     * @param string|null $multiNotice Message about intermediate revisions
407     *
408     * @return string
409     */
410    public static function diffToggle( Title $title, int $fromrev, int $torev, ?string $multiNotice = null ): string {
411        // Construct a link to the diff
412        $href = $title->getFullURL( [ 'diff' => $torev, 'oldid' => $fromrev ] );
413
414        $toggle = Html::element( 'a', [
415            'class' => 'fr-toggle-text',
416            'title' => wfMessage( 'revreview-diff-toggle-title' )->text(),
417            'href' => $href,
418            'data-mw-fromrev' => $fromrev,
419            'data-mw-torev' => $torev,
420            'data-mw-multinotice' => $multiNotice,
421        ], wfMessage( 'revreview-diff-toggle-show' )->text() );
422
423        return '<span id="mw-fr-diff-toggle">' .
424            wfMessage( 'parentheses' )->rawParams( $toggle )->escaped() . '</span>';
425    }
426
427    /**
428     * Creates a stability log excerpt
429     */
430    public static function stabilityLogExcerpt( Title $title ): string {
431        $logHtml = '';
432        $params = [
433            'lim'   => 1,
434            'flags' => LogEventsList::NO_EXTRA_USER_LINKS
435        ];
436        LogEventsList::showLogExtract( $logHtml, 'stable',
437            $title->getPrefixedText(), '', $params );
438        return Html::rawElement(
439            'div',
440            [ 'id' => 'mw-fr-logexcerpt' ],
441            $logHtml
442        );
443    }
444
445}