Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.11% covered (warning)
89.11%
90 / 101
83.33% covered (warning)
83.33%
15 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
AttributeManager
90.00% covered (success)
90.00%
90 / 100
83.33% covered (warning)
83.33%
15 / 18
47.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getUserCallable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getUserEnabledEvents
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 getUserEnabledEventsBySections
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 getEventsForSection
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 getInternalCategoryNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCategoryEligibility
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getNotificationPriority
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getCategoryPriority
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getNotificationCategory
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getEventsByCategory
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getNotifyTypeAvailabilityForCategory
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 isNotifyTypeAvailableForCategory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCategoryDisplayedInPreferences
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isNotifyTypeDismissableForCategory
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getNotificationSection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNotifyAgentEvents
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isBundleExpandable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5use MediaWiki\User\Options\UserOptionsLookup;
6use MediaWiki\User\UserGroupManager;
7use MediaWiki\User\UserIdentity;
8
9/**
10 * An object that manages attributes of echo notifications: category, eligibility,
11 * group, section etc.
12 */
13class AttributeManager {
14    /**
15     * @var UserGroupManager
16     */
17    private $userGroupManager;
18
19    /** @var UserOptionsLookup */
20    private $userOptionsLookup;
21
22    /**
23     * @var array[]
24     */
25    protected $notifications;
26
27    /**
28     * @var array[]
29     */
30    protected $categories;
31
32    /**
33     * @var bool[]
34     */
35    protected $defaultNotifyTypeAvailability;
36
37    /**
38     * @var array[]
39     */
40    protected $notifyTypeAvailabilityByCategory;
41
42    /**
43     * Notification section constant
44     */
45    public const ALERT = 'alert';
46    public const MESSAGE = 'message';
47    public const ALL = 'all';
48
49    /** @var string */
50    protected const DEFAULT_SECTION = self::ALERT;
51
52    /**
53     * Notifications are broken down to two sections, default is alert
54     * @var string[]
55     */
56    public static $sections = [
57        self::ALERT,
58        self::MESSAGE
59    ];
60
61    /**
62     * Names for keys in $wgEchoNotifications notification config
63     */
64    public const ATTR_LOCATORS = 'user-locators';
65    public const ATTR_FILTERS = 'user-filters';
66
67    /**
68     * @param array[] $notifications Notification attributes
69     * @param array[] $categories Notification categories
70     * @param bool[] $defaultNotifyTypeAvailability Associative array with output
71     *   formats as keys and whether they are available as boolean values.
72     * @param array[] $notifyTypeAvailabilityByCategory Associative array with
73     *   categories as keys and value an associative array as with
74     *   $defaultNotifyTypeAvailability.
75     * @param UserGroupManager $userGroupManager
76     * @param UserOptionsLookup $userOptionsLookup
77     */
78    public function __construct(
79        array $notifications,
80        array $categories,
81        array $defaultNotifyTypeAvailability,
82        array $notifyTypeAvailabilityByCategory,
83        UserGroupManager $userGroupManager,
84        UserOptionsLookup $userOptionsLookup
85    ) {
86        // Extensions can define their own notifications and categories
87        $this->notifications = $notifications;
88        $this->categories = $categories;
89
90        $this->defaultNotifyTypeAvailability = $defaultNotifyTypeAvailability;
91        $this->notifyTypeAvailabilityByCategory = $notifyTypeAvailabilityByCategory;
92        $this->userGroupManager = $userGroupManager;
93        $this->userOptionsLookup = $userOptionsLookup;
94    }
95
96    /**
97     * Get the user-locators|user-filters related to the provided event type
98     *
99     * @param string $type
100     * @param string $locator Either self::ATTR_LOCATORS or self::ATTR_FILTERS
101     * @return array
102     */
103    public function getUserCallable( $type, $locator = self::ATTR_LOCATORS ) {
104        if ( isset( $this->notifications[$type][$locator] ) ) {
105            return (array)$this->notifications[$type][$locator];
106        }
107
108        return [];
109    }
110
111    /**
112     * Get the enabled events for a user, which excludes user-dismissed events
113     * from the general enabled events
114     * @param UserIdentity $userIdentity
115     * @param string|string[] $notifierTypes a defined notifier type, or an array containing one
116     *   or more defined notifier types
117     * @return string[]
118     */
119    public function getUserEnabledEvents( UserIdentity $userIdentity, $notifierTypes ) {
120        if ( is_string( $notifierTypes ) ) {
121            $notifierTypes = [ $notifierTypes ];
122        }
123        return array_values( array_filter(
124            array_keys( $this->notifications ),
125            function ( $eventType ) use ( $userIdentity, $notifierTypes ) {
126                $category = $this->getNotificationCategory( $eventType );
127                return $this->getCategoryEligibility( $userIdentity, $category ) &&
128                    array_reduce( $notifierTypes, function ( $prev, $type ) use ( $userIdentity, $category ) {
129                        return $prev ||
130                            (
131                                $this->isNotifyTypeAvailableForCategory( $category, $type ) &&
132                                $this->userOptionsLookup->getOption(
133                                    $userIdentity,
134                                    "echo-subscriptions-$type-$category"
135                                )
136                            );
137                    }, false );
138            }
139        ) );
140    }
141
142    /**
143     * Get the user enabled events for the specified sections
144     * @param UserIdentity $userIdentity
145     * @param string|string[] $notifierTypes a defined notifier type, or an array containing one
146     *   or more defined notifier types
147     * @param string[] $sections
148     * @return string[]
149     */
150    public function getUserEnabledEventsBySections(
151        UserIdentity $userIdentity,
152        $notifierTypes,
153        array $sections
154    ) {
155        $events = [];
156        foreach ( $sections as $section ) {
157            $events = array_merge(
158                $events,
159                $this->getEventsForSection( $section )
160            );
161        }
162
163        return array_intersect(
164            $this->getUserEnabledEvents( $userIdentity, $notifierTypes ),
165            $events
166        );
167    }
168
169    /**
170     * Gets events (notification types) for a given section
171     *
172     * @param string $section Internal section name, one of the values from self::$sections
173     *
174     * @return string[] Array of notification types in this section
175     */
176    public function getEventsForSection( $section ) {
177        $events = [];
178
179        $isDefault = ( $section === self::DEFAULT_SECTION );
180
181        foreach ( $this->notifications as $event => $attribs ) {
182            if (
183                (
184                    isset( $attribs['section'] ) &&
185                    $attribs['section'] === $section
186                ) ||
187                (
188                    $isDefault &&
189                    (
190                        !isset( $attribs['section'] ) ||
191
192                        // Invalid section
193                        !in_array( $attribs['section'], self::$sections )
194                    )
195                )
196
197            ) {
198                $events[] = $event;
199            }
200        }
201
202        return $events;
203    }
204
205    /**
206     * Gets array of internal category names
207     *
208     * @return string[] All internal names
209     */
210    public function getInternalCategoryNames() {
211        return array_keys( $this->categories );
212    }
213
214    /**
215     * See if a user is eligible to receive a certain type of notification
216     * (based on user groups, not user preferences)
217     *
218     * @param UserIdentity $userIdentity
219     * @param string $category A notification category defined in $wgEchoNotificationCategories
220     * @return bool
221     */
222    public function getCategoryEligibility( UserIdentity $userIdentity, $category ) {
223        $usersGroups = $this->userGroupManager->getUserGroups( $userIdentity );
224        if ( isset( $this->categories[$category]['usergroups'] ) ) {
225            $allowedGroups = $this->categories[$category]['usergroups'];
226            if ( !array_intersect( $usersGroups, $allowedGroups ) ) {
227                return false;
228            }
229        }
230
231        return true;
232    }
233
234    /**
235     * Get the priority for a specific notification type
236     *
237     * @param string $notificationType A notification type defined in $wgEchoNotifications
238     * @return int From 1 to 10 (10 is default)
239     */
240    public function getNotificationPriority( $notificationType ) {
241        $category = $this->getNotificationCategory( $notificationType );
242
243        return $this->getCategoryPriority( $category );
244    }
245
246    /**
247     * Get the priority for a notification category
248     *
249     * @param string $category A notification category defined in $wgEchoNotificationCategories
250     * @return int From 1 to 10 (10 is default)
251     */
252    public function getCategoryPriority( $category ) {
253        if ( isset( $this->categories[$category]['priority'] ) ) {
254            $priority = $this->categories[$category]['priority'];
255            if ( $priority >= 1 && $priority <= 10 ) {
256                return $priority;
257            }
258        }
259
260        return 10;
261    }
262
263    /**
264     * Get the notification category for a notification type
265     *
266     * @param string $notificationType A notification type defined in $wgEchoNotifications
267     * @return string The name of the notification category or 'other' if no
268     *     category is explicitly assigned.
269     */
270    public function getNotificationCategory( $notificationType ) {
271        if ( isset( $this->notifications[$notificationType]['category'] ) ) {
272            $category = $this->notifications[$notificationType]['category'];
273            if ( isset( $this->categories[$category] ) ) {
274                return $category;
275            }
276        }
277
278        return 'other';
279    }
280
281    /**
282     * Gets an associative array mapping categories to the notification types in
283     * the category
284     *
285     * @return array[] Associative array with category as key
286     */
287    public function getEventsByCategory() {
288        $eventsByCategory = [];
289
290        foreach ( $this->categories as $category => $categoryDetails ) {
291            $eventsByCategory[$category] = [];
292        }
293
294        foreach ( $this->notifications as $notificationType => $notificationDetails ) {
295            $category = $notificationDetails['category'];
296            if ( isset( $eventsByCategory[$category] ) ) {
297                // Only real categories.  Currently, this excludes the 'foreign'
298                // pseudo-category.
299                $eventsByCategory[$category][] = $notificationType;
300            }
301        }
302
303        return $eventsByCategory;
304    }
305
306    /**
307     * Get notify type availability for all notify types for a given category.
308     *
309     * This means whether users *can* turn notifications for this category and format
310     * on, regardless of the default or a particular user's preferences.
311     *
312     * @param string $category Category name
313     * @return array [ 'web' => bool, 'email' => bool ]
314     */
315    public function getNotifyTypeAvailabilityForCategory( $category ) {
316        return array_merge(
317            $this->defaultNotifyTypeAvailability,
318            $this->notifyTypeAvailabilityByCategory[$category] ?? []
319        );
320    }
321
322    /**
323     * Checks whether the specified notify type is available for the specified
324     * category.
325     *
326     * This means whether users *can* turn notifications for this category and format
327     * on, regardless of the default or a particular user's preferences.
328     *
329     * @param string $category Category name
330     * @param string $notifyType notify type, e.g. email/web.
331     * @return bool
332     */
333    public function isNotifyTypeAvailableForCategory( $category, $notifyType ) {
334        return $this->getNotifyTypeAvailabilityForCategory( $category )[$notifyType];
335    }
336
337    /**
338     * Checks whether category is displayed in preferences
339     *
340     * @param string $category Category name
341     * @return bool
342     */
343    public function isCategoryDisplayedInPreferences( $category ) {
344        return !(
345            isset( $this->categories[$category]['no-dismiss'] ) &&
346            in_array( 'all', $this->categories[$category]['no-dismiss'] )
347        );
348    }
349
350    /**
351     * Checks whether the specified notify type is dismissable for the specified
352     * category.
353     *
354     * This means whether the user is allowed to opt out of receiving notifications
355     * for this category and format.
356     *
357     * @param string $category Name of category
358     * @param string $notifyType notify type, e.g. email/web.
359     * @return bool
360     */
361    public function isNotifyTypeDismissableForCategory( $category, $notifyType ) {
362        return !(
363            isset( $this->categories[$category]['no-dismiss'] ) &&
364            (
365                in_array( 'all', $this->categories[$category]['no-dismiss'] ) ||
366                in_array( $notifyType, $this->categories[$category]['no-dismiss'] )
367            )
368        );
369    }
370
371    /**
372     * Get notification section for a notification type
373     * @param string $notificationType
374     * @return string
375     */
376    public function getNotificationSection( $notificationType ) {
377        return $this->notifications[$notificationType]['section'] ?? self::DEFAULT_SECTION;
378    }
379
380    /**
381     * Get notification types that allow their own agent to be notified.
382     *
383     * @return string[] Notification types
384     */
385    public function getNotifyAgentEvents() {
386        $events = [];
387        foreach ( $this->notifications as $event => $attribs ) {
388            if ( $attribs['canNotifyAgent'] ?? false ) {
389                $events[] = $event;
390            }
391        }
392        return $events;
393    }
394
395    /**
396     * @param string $type
397     * @return bool Whether a notification type can be an expandable bundle
398     */
399    public function isBundleExpandable( $type ) {
400        return $this->notifications[$type]['bundle']['expandable'] ?? false;
401    }
402
403}
404
405class_alias( AttributeManager::class, 'EchoAttributeManager' );