Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 183
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialDisplayNotificationsConfiguration
0.00% covered (danger)
0.00%
0 / 183
0.00% covered (danger)
0.00%
0 / 10
992
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 outputConfiguration
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 outputCheckMatrix
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 outputNotificationsInCategories
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 outputNotificationsInSections
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 outputAvailability
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getNewUserPreferenceOverrides
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 outputEnabledDefault
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
110
 outputMandatory
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\Notifications\Special;
4
5use MediaWiki\Extension\Notifications\AttributeManager;
6use MediaWiki\Extension\Notifications\Hooks as EchoHooks;
7use MediaWiki\Html\Html;
8use MediaWiki\SpecialPage\UnlistedSpecialPage;
9use MediaWiki\User\Options\UserOptionsManager;
10use MediaWiki\User\User;
11use OOUIHTMLForm;
12
13class SpecialDisplayNotificationsConfiguration extends UnlistedSpecialPage {
14    /**
15     * AttributeManager to access notification configuration
16     *
17     * @var AttributeManager
18     */
19    protected $attributeManager;
20
21    /**
22     * Category names, mapping internal name to HTML-formatted name
23     *
24     * @var string[]
25     */
26    protected $categoryNames;
27
28    // Should be one mapping text (friendly) name to internal name, but there
29    // is no friendly name
30    /**
31     * Notification type names.  Mapping HTML-formatted internal name to internal name
32     *
33     * @var string[]
34     */
35    protected $notificationTypeNames;
36
37    /**
38     * Notify types, mapping internal name to HTML-formatted name
39     *
40     * @var string[]
41     */
42    protected $notifyTypes;
43
44    // Due to how HTMLForm works, it's convenient to have both directions
45    /**
46     * Category names, mapping HTML-formatted name to internal name
47     *
48     * @var string[]
49     */
50    protected $flippedCategoryNames;
51
52    /**
53     * Notify types, mapping HTML-formatted name to internal name
54     *
55     * @var string[]
56     */
57    protected $flippedNotifyTypes;
58
59    /**
60     * @var UserOptionsManager
61     */
62    private $userOptionsManager;
63
64    /**
65     * @param AttributeManager $attributeManager
66     * @param UserOptionsManager $userOptionsManager
67     */
68    public function __construct(
69        AttributeManager $attributeManager,
70        UserOptionsManager $userOptionsManager
71    ) {
72        parent::__construct( 'DisplayNotificationsConfiguration' );
73
74        $this->attributeManager = $attributeManager;
75        $this->userOptionsManager = $userOptionsManager;
76    }
77
78    public function execute( $subPage ) {
79        $this->setHeaders();
80        $this->checkPermissions();
81
82        $config = $this->getConfig();
83
84        $internalCategoryNames = $this->attributeManager->getInternalCategoryNames();
85        $this->categoryNames = [];
86
87        foreach ( $internalCategoryNames as $internalCategoryName ) {
88            $formattedFriendlyCategoryName = Html::element(
89                'strong',
90                [],
91                $this->msg( 'echo-category-title-' . $internalCategoryName )->numParams( 1 )->text()
92            );
93
94            $formattedInternalCategoryName = $this->msg( 'parentheses' )->rawParams(
95                Html::element(
96                    'em',
97                    [],
98                    $internalCategoryName
99                )
100            )->parse();
101
102            $this->categoryNames[$internalCategoryName] = $formattedFriendlyCategoryName . ' '
103                . $formattedInternalCategoryName;
104        }
105
106        $this->flippedCategoryNames = array_flip( $this->categoryNames );
107
108        $this->notifyTypes = [];
109        foreach ( $config->get( 'EchoNotifiers' ) as $notifyType => $notifier ) {
110            $this->notifyTypes[$notifyType] = $this->msg( 'echo-pref-' . $notifyType )->escaped();
111        }
112
113        $this->flippedNotifyTypes = array_flip( $this->notifyTypes );
114
115        $notificationTypes = array_keys( $config->get( 'EchoNotifications' ) );
116        $this->notificationTypeNames = array_combine(
117            array_map( 'htmlspecialchars', $notificationTypes ),
118            $notificationTypes
119        );
120
121        $this->getOutput()->setPageTitleMsg( $this->msg( 'echo-displaynotificationsconfiguration' ) );
122        $this->outputHeader( 'echo-displaynotificationsconfiguration-summary' );
123        $this->outputConfiguration();
124    }
125
126    /**
127     * Outputs the Echo configuration
128     */
129    protected function outputConfiguration() {
130        $this->outputNotificationsInCategories();
131        $this->outputNotificationsInSections();
132        $this->outputAvailability();
133        $this->outputMandatory();
134        $this->outputEnabledDefault();
135    }
136
137    /**
138     * Displays a checkbox matrix, using an HTMLForm
139     *
140     * @param string $id Arbitrary ID
141     * @param string $legendMsgKey Message key for an explanatory legend.  For example,
142     *   "We wrote this feature because in the days of yore, there was but one notification badge"
143     * @param array $rowLabelMapping Associative array mapping label to tag
144     * @param array $columnLabelMapping Associative array mapping label to tag
145     * @param array $value Array consisting of strings in the format '$columnTag-$rowTag'
146     */
147    protected function outputCheckMatrix(
148        $id,
149        $legendMsgKey,
150        array $rowLabelMapping,
151        array $columnLabelMapping,
152        array $value
153    ) {
154        $form = new OOUIHTMLForm(
155            [
156                $id => [
157                    'type' => 'checkmatrix',
158                    'rows' => $rowLabelMapping,
159                    'columns' => $columnLabelMapping,
160                    'default' => $value,
161                    'disabled' => true,
162                ]
163            ],
164            $this->getContext()
165        );
166
167        $form->setTitle( $this->getPageTitle() )
168            ->prepareForm()
169            ->suppressDefaultSubmit()
170            ->setWrapperLegendMsg( $legendMsgKey )
171            ->displayForm( false );
172    }
173
174    /**
175     * Outputs the notification types in each category
176     */
177    protected function outputNotificationsInCategories() {
178        $notificationsByCategory = $this->attributeManager->getEventsByCategory();
179
180        $out = $this->getOutput();
181        $out->addHTML( Html::element(
182            'h2',
183            [ 'id' => 'mw-echo-displaynotificationsconfiguration-notifications-by-category' ],
184            $this->msg( 'echo-displaynotificationsconfiguration-notifications-by-category-header' )->text()
185        ) );
186
187        $out->addHTML( Html::openElement( 'ul' ) );
188        foreach ( $notificationsByCategory as $categoryName => $notificationTypes ) {
189            $implodedTypes = Html::element(
190                'span',
191                [],
192                implode( $this->msg( 'comma-separator' )->text(), $notificationTypes )
193            );
194
195            $out->addHTML(
196                Html::rawElement(
197                    'li',
198                    [],
199                    $this->categoryNames[$categoryName] . $this->msg( 'colon-separator' )->escaped() . ' '
200                        . $implodedTypes
201                )
202            );
203        }
204        $out->addHTML( Html::closeElement( 'ul' ) );
205    }
206
207    /**
208     * Output the notification types in each section (alert/message)
209     */
210    protected function outputNotificationsInSections() {
211        $this->getOutput()->addHTML( Html::element(
212            'h2',
213            [ 'id' => 'mw-echo-displaynotificationsconfiguration-sorting-by-section' ],
214            $this->msg( 'echo-displaynotificationsconfiguration-sorting-by-section-header' )->text()
215        ) );
216
217        $bySectionValue = [];
218
219        $flippedSectionNames = [];
220
221        foreach ( AttributeManager::$sections as $section ) {
222            $types = $this->attributeManager->getEventsForSection( $section );
223            // echo-notification-alert-text-only, echo-notification-notice-text-only
224            $msgSection = $section == 'message' ? 'notice' : $section;
225            $flippedSectionNames[$this->msg( 'echo-notification-' . $msgSection . '-text-only' )->escaped()]
226                = $section;
227            foreach ( $types as $type ) {
228                $bySectionValue[] = "$section-$type";
229            }
230        }
231
232        $this->outputCheckMatrix(
233            'type-by-section',
234            'echo-displaynotificationsconfiguration-sorting-by-section-legend',
235            $this->notificationTypeNames,
236            $flippedSectionNames,
237            $bySectionValue
238        );
239    }
240
241    /**
242     * Output which notify types are available for each category
243     */
244    protected function outputAvailability() {
245        $this->getOutput()->addHTML( Html::element(
246            'h2',
247            [ 'id' => 'mw-echo-displaynotificationsconfiguration-available-notification-methods' ],
248            $this->msg( 'echo-displaynotificationsconfiguration-available-notification-methods-header' )->text()
249        ) );
250
251        $byCategoryValue = [];
252
253        foreach ( $this->notifyTypes as $notifyType => $displayNotifyType ) {
254            foreach ( $this->categoryNames as $category => $displayCategory ) {
255                if ( $this->attributeManager->isNotifyTypeAvailableForCategory( $category, $notifyType ) ) {
256                    $byCategoryValue[] = "$notifyType-$category";
257                }
258            }
259        }
260
261        $this->outputCheckMatrix(
262            'availability-by-category',
263            'echo-displaynotificationsconfiguration-available-notification-methods-by-category-legend',
264            $this->flippedCategoryNames,
265            $this->flippedNotifyTypes,
266            $byCategoryValue
267        );
268    }
269
270    /**
271     * View-only overrides of notification preferences for new users
272     *
273     * @todo (Likely) remove the underlying functionality, see
274     * https://phabricator.wikimedia.org/T357219.
275     * @return bool[]
276     */
277    private static function getNewUserPreferenceOverrides(): array {
278        return [
279            'echo-subscriptions-web-reverted' => false,
280            'echo-subscriptions-web-article-linked' => true,
281            'echo-subscriptions-email-mention' => true,
282            'echo-subscriptions-email-article-linked' => true,
283        ];
284    }
285
286    /**
287     * Output which notification categories are turned on by default, for each notify type
288     */
289    protected function outputEnabledDefault() {
290        $this->getOutput()->addHTML( Html::element(
291            'h2',
292            [ 'id' => 'mw-echo-displaynotificationsconfiguration-enabled-default' ],
293            $this->msg( 'echo-displaynotificationsconfiguration-enabled-default-header' )->text()
294        ) );
295
296        // Some of the preferences are mapped to existing ones defined in core MediaWiki
297        $virtualOptions = EchoHooks::getVirtualUserOptions();
298
299        // In reality, anon users are not relevant to Echo, but this lets us easily query default options.
300        $anonUser = new User;
301
302        $byCategoryValueExisting = [];
303        foreach ( $this->notifyTypes as $notifyType => $displayNotifyType ) {
304            foreach ( $this->categoryNames as $category => $displayCategory ) {
305                $prefKey = "echo-subscriptions-$notifyType-$category";
306                if ( isset( $virtualOptions[ $prefKey ] ) ) {
307                    $prefKey = $virtualOptions[ $prefKey ];
308                }
309                if ( $this->userOptionsManager->getOption( $anonUser, $prefKey ) ) {
310                    $byCategoryValueExisting[] = "$notifyType-$category";
311                }
312            }
313        }
314
315        $this->outputCheckMatrix(
316            'enabled-by-default-generic',
317            'echo-displaynotificationsconfiguration-enabled-default-existing-users-legend',
318            $this->flippedCategoryNames,
319            $this->flippedNotifyTypes,
320            $byCategoryValueExisting
321        );
322
323        $loggedInUser = new User;
324
325        // NOTE: This is not reliable, and will break if other variables than those hardcoded in
326        // the method below are changed for new users, either via a hook or via conditional
327        // defaults. See T357219 for details.
328        $overrides = $this->getNewUserPreferenceOverrides();
329        foreach ( $overrides as $prefKey => $value ) {
330            $this->userOptionsManager->setOption( $loggedInUser, $prefKey, $value );
331        }
332
333        $byCategoryValueNew = [];
334        foreach ( $this->notifyTypes as $notifyType => $displayNotifyType ) {
335            foreach ( $this->categoryNames as $category => $displayCategory ) {
336                $prefKey = "echo-subscriptions-$notifyType-$category";
337                if ( isset( $virtualOptions[ $prefKey ] ) ) {
338                    $prefKey = $virtualOptions[ $prefKey ];
339                }
340                if ( $this->userOptionsManager->getOption( $loggedInUser, $prefKey ) ) {
341                    $byCategoryValueNew[] = "$notifyType-$category";
342                }
343            }
344        }
345
346        $this->outputCheckMatrix(
347            'enabled-by-default-new',
348            'echo-displaynotificationsconfiguration-enabled-default-new-users-legend',
349            $this->flippedCategoryNames,
350            $this->flippedNotifyTypes,
351            $byCategoryValueNew
352        );
353    }
354
355    /**
356     * Output which notify types are mandatory for each category
357     */
358    protected function outputMandatory() {
359        $byCategoryValue = [];
360
361        $this->getOutput()->addHTML( Html::element(
362            'h2',
363            [ 'id' => 'mw-echo-displaynotificationsconfiguration-mandatory-notification-methods' ],
364            $this->msg( 'echo-displaynotificationsconfiguration-mandatory-notification-methods-header' )->text()
365        ) );
366
367        foreach ( $this->notifyTypes as $notifyType => $displayNotifyType ) {
368            foreach ( $this->categoryNames as $category => $displayCategory ) {
369                if ( !$this->attributeManager->isNotifyTypeDismissableForCategory( $category, $notifyType ) ) {
370                    $byCategoryValue[] = "$notifyType-$category";
371                }
372            }
373        }
374
375        $this->outputCheckMatrix(
376            'mandatory',
377            'echo-displaynotificationsconfiguration-mandatory-notification-methods-by-category-legend',
378            $this->flippedCategoryNames,
379            $this->flippedNotifyTypes,
380            $byCategoryValue
381        );
382    }
383}