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