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