Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.07% covered (warning)
58.07%
187 / 322
13.33% covered (danger)
13.33%
2 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiEchoNotifications
58.07% covered (warning)
58.07%
187 / 322
13.33% covered (danger)
13.33%
2 / 15
658.69
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 execute
64.29% covered (warning)
64.29%
18 / 28
0.00% covered (danger)
0.00%
0 / 1
20.70
 getLocalNotifications
70.21% covered (warning)
70.21%
33 / 47
0.00% covered (danger)
0.00%
0 / 1
17.47
 getSectionPropList
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
2.19
 getPropList
30.43% covered (danger)
30.43%
14 / 46
0.00% covered (danger)
0.00%
0 / 1
127.07
 getPropCount
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
4
 getPropSeenTime
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 makeForeignNotification
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
 getForeignQueryParams
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 mergeResults
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 mergeList
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
9.95
 mergeCount
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
8.14
 getAllowedParams
92.31% covered (success)
92.31%
72 / 78
0.00% covered (danger)
0.00%
0 / 1
3.00
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Notifications\Api;
4
5use ApiBase;
6use ApiQuery;
7use ApiQueryBase;
8use MediaWiki\Config\Config;
9use MediaWiki\Extension\Notifications\AttributeManager;
10use MediaWiki\Extension\Notifications\Bundler;
11use MediaWiki\Extension\Notifications\Controller\NotificationController;
12use MediaWiki\Extension\Notifications\DataOutputFormatter;
13use MediaWiki\Extension\Notifications\ForeignNotifications;
14use MediaWiki\Extension\Notifications\Mapper\NotificationMapper;
15use MediaWiki\Extension\Notifications\Model\Notification;
16use MediaWiki\Extension\Notifications\NotifUser;
17use MediaWiki\Extension\Notifications\SeenTime;
18use MediaWiki\Extension\Notifications\Services;
19use MediaWiki\Title\Title;
20use MediaWiki\User\User;
21use MediaWiki\WikiMap\WikiMap;
22use Wikimedia\ParamValidator\ParamValidator;
23use Wikimedia\ParamValidator\TypeDef\IntegerDef;
24
25class ApiEchoNotifications extends ApiQueryBase {
26    use ApiCrossWiki;
27
28    /**
29     * @var bool
30     */
31    protected $crossWikiSummary = false;
32
33    /** @var string[] */
34    private $allowedNotifierTypes;
35
36    public function __construct( ApiQuery $query, string $moduleName, Config $mainConfig ) {
37        parent::__construct( $query, $moduleName, 'not' );
38        $this->allowedNotifierTypes = array_keys( $mainConfig->get( 'EchoNotifiers' ) );
39    }
40
41    public function execute() {
42        // To avoid API warning, register the parameter used to bust browser cache
43        $this->getMain()->getVal( '_' );
44
45        if ( !$this->getUser()->isRegistered() ) {
46            $this->dieWithError( 'apierror-mustbeloggedin-generic', 'login-required' );
47        }
48
49        $params = $this->extractRequestParams();
50
51        /* @deprecated */
52        if ( $params['format'] === 'flyout' ) {
53            $this->addDeprecation( 'apiwarn-echo-deprecation-flyout',
54                'action=query&meta=notifications&notformat=flyout' );
55        } elseif ( $params['format'] === 'html' ) {
56            $this->addDeprecation( 'apiwarn-echo-deprecation-html',
57                'action=query&meta=notifications&notformat=html' );
58        }
59
60        if ( $this->allowCrossWikiNotifications() ) {
61            $this->crossWikiSummary = $params['crosswikisummary'];
62        }
63
64        $results = [];
65        if ( in_array( WikiMap::getCurrentWikiId(), $this->getRequestedWikis() ) ) {
66            $results[WikiMap::getCurrentWikiId()] = $this->getLocalNotifications( $params );
67        }
68
69        if ( $this->getRequestedForeignWikis() ) {
70            $foreignResults = $this->getFromForeign();
71            foreach ( $foreignResults as $wiki => $result ) {
72                if ( isset( $result['query']['notifications'] ) ) {
73                    $results[$wiki] = $result['query']['notifications'];
74                }
75            }
76        }
77
78        // after getting local & foreign results, merge them all together
79        $result = $this->mergeResults( $results, $params );
80        if ( $params['groupbysection'] ) {
81            foreach ( $params['sections'] as $section ) {
82                if ( in_array( 'list', $params['prop'] ) ) {
83                    $this->getResult()->setIndexedTagName( $result[$section]['list'], 'notification' );
84                }
85            }
86        } else {
87            if ( in_array( 'list', $params['prop'] ) ) {
88                $this->getResult()->setIndexedTagName( $result['list'], 'notification' );
89            }
90        }
91        $this->getResult()->addValue( 'query', $this->getModuleName(), $result );
92    }
93
94    /**
95     * @param array $params
96     * @return array
97     */
98    protected function getLocalNotifications( array $params ) {
99        $user = $this->getUser();
100        $prop = $params['prop'];
101        $titles = null;
102        if ( $params['titles'] ) {
103            $titles = array_values( array_filter( array_map( [ Title::class, 'newFromText' ], $params['titles'] ) ) );
104            if ( in_array( '[]', $params['titles'] ) ) {
105                $titles[] = null;
106            }
107        }
108
109        $result = [];
110        if ( in_array( 'list', $prop ) ) {
111            // Group notification results by section
112            if ( $params['groupbysection'] ) {
113                foreach ( $params['sections'] as $section ) {
114                    $result[$section] = $this->getSectionPropList(
115                        $user, $section, $params['filter'], $params['limit'],
116                        $params[$section . 'continue'], $params['format'],
117                        $titles, $params[$section . 'unreadfirst'], $params['bundle'],
118                        $params['notifiertypes']
119                    );
120
121                    if ( $this->crossWikiSummary ) {
122                        // insert fake notification for foreign notifications
123                        $foreignNotification = $this->makeForeignNotification( $user, $params['format'], $section );
124                        if ( $foreignNotification ) {
125                            array_unshift( $result[$section]['list'], $foreignNotification );
126                        }
127                    }
128                }
129            } else {
130                $attributeManager = Services::getInstance()->getAttributeManager();
131                $result = $this->getPropList(
132                    $user,
133                    $attributeManager->getUserEnabledEventsBySections( $user, $params['notifiertypes'],
134                        $params['sections'] ),
135                    $params['filter'], $params['limit'], $params['continue'], $params['format'],
136                    $titles, $params['unreadfirst'], $params['bundle']
137                );
138
139                // if exactly 1 section is specified, we consider only that section, otherwise
140                // we pass ALL to consider all foreign notifications
141                $section = count( $params['sections'] ) === 1
142                    ? reset( $params['sections'] )
143                    : AttributeManager::ALL;
144                if ( $this->crossWikiSummary ) {
145                    $foreignNotification = $this->makeForeignNotification( $user, $params['format'], $section );
146                    if ( $foreignNotification ) {
147                        array_unshift( $result['list'], $foreignNotification );
148                    }
149                }
150            }
151        }
152
153        if ( in_array( 'count', $prop ) ) {
154            $result = array_merge_recursive(
155                $result,
156                $this->getPropCount( $user, $params['sections'], $params['groupbysection'] )
157            );
158        }
159
160        if ( in_array( 'seenTime', $prop ) ) {
161            $result = array_merge_recursive(
162                $result,
163                $this->getPropSeenTime( $user, $params['sections'], $params['groupbysection'] )
164            );
165        }
166
167        return $result;
168    }
169
170    /**
171     * Internal method for getting the property 'list' data for individual section
172     * @param User $user
173     * @param string $section 'alert' or 'message'
174     * @param string[] $filter 'all', 'read' or 'unread'
175     * @param int $limit
176     * @param string $continue
177     * @param string $format
178     * @param Title[]|null $titles
179     * @param bool $unreadFirst
180     * @param bool $bundle
181     * @param string[] $notifierTypes
182     * @return array
183     */
184    protected function getSectionPropList(
185        User $user,
186        $section,
187        $filter,
188        $limit,
189        $continue,
190        $format,
191        array $titles = null,
192        $unreadFirst = false,
193        $bundle = false,
194        array $notifierTypes = [ 'web' ]
195    ) {
196        $attributeManager = Services::getInstance()->getAttributeManager();
197        $sectionEvents = $attributeManager->getUserEnabledEventsBySections( $user, $notifierTypes, [ $section ] );
198
199        if ( !$sectionEvents ) {
200            $result = [
201                'list' => [],
202                'continue' => null
203            ];
204        } else {
205            $result = $this->getPropList(
206                $user, $sectionEvents, $filter, $limit, $continue, $format, $titles, $unreadFirst, $bundle
207            );
208        }
209
210        return $result;
211    }
212
213    /**
214     * Internal helper method for getting property 'list' data, this is based
215     * on the event types specified in the arguments and it could be event types
216     * of a set of sections or a single section
217     * @param User $user
218     * @param string[] $eventTypes
219     * @param string[] $filter 'all', 'read' or 'unread'
220     * @param int $limit
221     * @param string $continue
222     * @param string $format
223     * @param Title[]|null $titles
224     * @param bool $unreadFirst
225     * @param bool $bundle
226     * @return array
227     */
228    protected function getPropList(
229        User $user,
230        array $eventTypes,
231        $filter,
232        $limit,
233        $continue,
234        $format,
235        array $titles = null,
236        $unreadFirst = false,
237        $bundle = false
238    ) {
239        $result = [
240            'list' => [],
241            'continue' => null
242        ];
243
244        $notifMapper = new NotificationMapper();
245
246        // check if we want both read & unread...
247        if ( in_array( 'read', $filter ) && in_array( '!read', $filter ) ) {
248            // Prefer unread notifications. We don't care about next offset in this case
249            if ( $unreadFirst ) {
250                // query for unread notifications past 'continue' (offset)
251                $notifs = $notifMapper->fetchUnreadByUser( $user, $limit + 1, $continue, $eventTypes, $titles );
252
253                /*
254                 * 'continue' has a timestamp & id (to start with, in case
255                 * there would be multiple events with that same timestamp)
256                 * Unread notifications should always load first, but may be
257                 * older than read ones, but we can work with current
258                 * 'continue' format:
259                 * * if there's no continue, first load unread notifications
260                 * * if there's a continue, fetch unread notifications first
261                 * * if there are no unread ones, continue must've been
262                 *   about read notifications: fetch 'em
263                 * * if there are unread ones but first one doesn't match
264                 *   continue id, it must've been about read notifications:
265                 *   discard unread & fetch read
266                 */
267                if ( $notifs && $continue ) {
268                    /** @var Notification $first */
269                    $first = reset( $notifs );
270                    $continueId = intval( trim( strrchr( $continue, '|' ), '|' ) );
271                    if ( $first->getEvent()->getId() !== $continueId ) {
272                        // notification doesn't match continue id, it must've been
273                        // about read notifications: discard all unread ones
274                        $notifs = [];
275                    }
276                }
277
278                // If there are less unread notifications than we requested,
279                // then fill the result with some read notifications
280                $count = count( $notifs );
281                // we need 1 more than $limit, so we can respond 'continue'
282                if ( $count <= $limit ) {
283                    // Query planner should be smart enough that passing a short list of ids to exclude
284                    // will only visit at most that number of extra rows.
285                    $mixedNotifs = $notifMapper->fetchByUser(
286                        $user,
287                        $limit - $count + 1,
288                        // if there were unread notifications, 'continue' was for
289                        // unread notifications and we should start fetching read
290                        // notifications from start
291                        $count > 0 ? null : $continue,
292                        $eventTypes,
293                        array_keys( $notifs ),
294                        $titles
295                    );
296                    foreach ( $mixedNotifs as $notif ) {
297                        $notifs[$notif->getEvent()->getId()] = $notif;
298                    }
299                }
300            } else {
301                $notifs = $notifMapper->fetchByUser( $user, $limit + 1, $continue, $eventTypes, [], $titles );
302            }
303        } elseif ( in_array( 'read', $filter ) ) {
304            $notifs = $notifMapper->fetchReadByUser( $user, $limit + 1, $continue, $eventTypes, $titles );
305        } else {
306            // = if ( in_array( '!read', $filter ) ) {
307            $notifs = $notifMapper->fetchUnreadByUser( $user, $limit + 1, $continue, $eventTypes, $titles );
308        }
309
310        // get $overfetchedItem before bundling and rendering so that it is not affected by filtering
311        /** @var Notification $overfetchedItem */
312        $overfetchedItem = count( $notifs ) > $limit ? array_pop( $notifs ) : null;
313
314        $bundler = null;
315        if ( $bundle ) {
316            $bundler = new Bundler();
317            $notifs = $bundler->bundle( $notifs );
318        }
319
320        while ( $notifs !== [] ) {
321            /** @var Notification $notif */
322            $notif = array_shift( $notifs );
323            $output = DataOutputFormatter::formatOutput( $notif, $format, $user, $this->getLanguage() );
324            if ( $output !== false ) {
325                $result['list'][] = $output;
326            } elseif ( $bundler && $notif->getBundledNotifications() ) {
327                // when the bundle_base gets filtered out, bundled notifications
328                // have to be re-bundled and formatted
329                $notifs = array_merge( $bundler->bundle( $notif->getBundledNotifications() ), $notifs );
330            }
331        }
332
333        // Generate offset if necessary
334        if ( $overfetchedItem ) {
335            // @todo: what to do with this when fetching from multiple wikis?
336            $timestamp = wfTimestamp( TS_UNIX, $overfetchedItem->getTimestamp() );
337            $id = $overfetchedItem->getEvent()->getId();
338            $result['continue'] = $timestamp . '|' . $id;
339        }
340
341        return $result;
342    }
343
344    /**
345     * Internal helper method for getting property 'count' data
346     * @param User $user
347     * @param string[] $sections
348     * @param bool $groupBySection
349     * @return array
350     */
351    protected function getPropCount( User $user, array $sections, $groupBySection ) {
352        $result = [];
353        $notifUser = NotifUser::newFromUser( $user );
354        $global = $this->crossWikiSummary ? 'preference' : false;
355
356        $totalRawCount = 0;
357        foreach ( $sections as $section ) {
358            $rawCount = $notifUser->getNotificationCount( $section, $global );
359            if ( $groupBySection ) {
360                $result[$section]['rawcount'] = $rawCount;
361                $result[$section]['count'] = NotificationController::formatNotificationCount( $rawCount );
362            }
363            $totalRawCount += $rawCount;
364        }
365        $result['rawcount'] = $totalRawCount;
366        $result['count'] = NotificationController::formatNotificationCount( $totalRawCount );
367
368        return $result;
369    }
370
371    /**
372     * Internal helper method for getting property 'seenTime' data
373     * @param User $user
374     * @param string[] $sections
375     * @param bool $groupBySection
376     * @return array
377     */
378    protected function getPropSeenTime( User $user, array $sections, $groupBySection ) {
379        $result = [];
380        $seenTimeHelper = SeenTime::newFromUser( $user );
381
382        if ( $groupBySection ) {
383            foreach ( $sections as $section ) {
384                $result[$section]['seenTime'] = $seenTimeHelper->getTime( $section, TS_ISO_8601 );
385            }
386        } else {
387            $result['seenTime'] = [];
388            foreach ( $sections as $section ) {
389                $result['seenTime'][$section] = $seenTimeHelper->getTime( $section, TS_ISO_8601 );
390            }
391        }
392
393        return $result;
394    }
395
396    /**
397     * Build and format a "fake" notification to represent foreign notifications.
398     * @param User $user
399     * @param string $format
400     * @param string $section
401     * @return array|false A formatted notification, or false if there are no foreign notifications
402     */
403    protected function makeForeignNotification(
404        User $user,
405        $format,
406        $section = AttributeManager::ALL
407    ) {
408        $wikis = $this->getForeignNotifications()->getWikis( $section );
409        $count = $this->getForeignNotifications()->getCount( $section );
410        $maxTimestamp = $this->getForeignNotifications()->getTimestamp( $section );
411        $timestampsByWiki = [];
412        foreach ( $wikis as $wiki ) {
413            $timestampsByWiki[$wiki] = $this->getForeignNotifications()->getWikiTimestamp( $wiki, $section );
414        }
415
416        if ( $count === 0 || $wikis === [] ) {
417            return false;
418        }
419
420        // Sort wikis by timestamp, in descending order (newest first)
421        usort( $wikis, static function ( $a, $b ) use ( $section, $timestampsByWiki ) {
422            return (int)$timestampsByWiki[$b]->getTimestamp( TS_UNIX )
423                - (int)$timestampsByWiki[$a]->getTimestamp( TS_UNIX );
424        } );
425
426        $row = (object)[
427            'event_id' => -1,
428            'event_type' => 'foreign',
429            'event_variant' => null,
430            'event_agent_id' => $user->getId(),
431            'event_agent_ip' => null,
432            'event_page_id' => null,
433            'event_extra' => serialize( [
434                'section' => $section ?: 'all',
435                'wikis' => $wikis,
436                'count' => $count
437            ] ),
438            'event_deleted' => 0,
439
440            'notification_user' => $user->getId(),
441            'notification_timestamp' => $maxTimestamp,
442            'notification_read_timestamp' => null,
443            'notification_bundle_hash' => md5( 'bogus' ),
444        ];
445
446        // Format output like any other notification
447        $notif = Notification::newFromRow( $row );
448        $output = DataOutputFormatter::formatOutput( $notif, $format, $user, $this->getLanguage() );
449
450        // Add cross-wiki-specific data
451        $output['section'] = $section ?: 'all';
452        $output['count'] = $count;
453        $output['sources'] = ForeignNotifications::getApiEndpoints( $wikis );
454        // Add timestamp information
455        foreach ( $output['sources'] as $wiki => &$data ) {
456            $data['ts'] = $timestampsByWiki[$wiki]->getTimestamp( TS_ISO_8601 );
457        }
458        return $output;
459    }
460
461    protected function getForeignQueryParams() {
462        $params = $this->getRequest()->getValues();
463
464        // don't request cross-wiki notification summaries
465        unset( $params['notcrosswikisummary'] );
466
467        return $params;
468    }
469
470    /**
471     * @param array[] $results
472     * @param array $params
473     * @return array
474     */
475    protected function mergeResults( array $results, array $params ) {
476        $primary = array_shift( $results );
477        if ( !$primary ) {
478            $primary = [];
479        }
480
481        if ( in_array( 'list', $params['prop'] ) ) {
482            $primary = $this->mergeList( $primary, $results, $params['groupbysection'] );
483        }
484
485        if ( in_array( 'count', $params['prop'] ) && !$this->crossWikiSummary ) {
486            // if crosswiki data was requested, the count in $primary
487            // is accurate already
488            // otherwise, we'll want to combine counts for all wikis
489            $primary = $this->mergeCount( $primary, $results, $params['groupbysection'] );
490        }
491
492        return $primary;
493    }
494
495    /**
496     * @param array $primary
497     * @param array[] $results
498     * @param bool $groupBySection
499     * @return array
500     */
501    protected function mergeList( array $primary, array $results, $groupBySection ) {
502        // sort all notifications by timestamp: most recent first
503        $sort = static function ( $a, $b ) {
504            return $a['timestamp']['utcunix'] - $b['timestamp']['utcunix'];
505        };
506
507        if ( $groupBySection ) {
508            foreach ( AttributeManager::$sections as $section ) {
509                if ( !isset( $primary[$section]['list'] ) ) {
510                    $primary[$section]['list'] = [];
511                }
512                foreach ( $results as $result ) {
513                    $primary[$section]['list'] = array_merge( $primary[$section]['list'], $result[$section]['list'] );
514                }
515                usort( $primary[$section]['list'], $sort );
516            }
517        } else {
518            if ( !isset( $primary['list'] ) || !is_array( $primary['list'] ) ) {
519                $primary['list'] = [];
520            }
521            foreach ( $results as $result ) {
522                $primary['list'] = array_merge( $primary['list'], $result['list'] );
523            }
524            usort( $primary['list'], $sort );
525        }
526
527        return $primary;
528    }
529
530    /**
531     * @param array $primary
532     * @param array[] $results
533     * @param bool $groupBySection
534     * @return array
535     */
536    protected function mergeCount( array $primary, array $results, $groupBySection ) {
537        if ( $groupBySection ) {
538            foreach ( AttributeManager::$sections as $section ) {
539                if ( !isset( $primary[$section]['rawcount'] ) ) {
540                    $primary[$section]['rawcount'] = 0;
541                }
542                foreach ( $results as $result ) {
543                    $primary[$section]['rawcount'] += $result[$section]['rawcount'];
544                }
545                $primary[$section]['count'] = NotificationController::formatNotificationCount(
546                    $primary[$section]['rawcount'] );
547            }
548        }
549
550        if ( !isset( $primary['rawcount'] ) ) {
551            $primary['rawcount'] = 0;
552        }
553        foreach ( $results as $result ) {
554            // regardless of groupbysection, totals are always included
555            $primary['rawcount'] += $result['rawcount'];
556        }
557        $primary['count'] = NotificationController::formatNotificationCount( $primary['rawcount'] );
558
559        return $primary;
560    }
561
562    public function getAllowedParams() {
563        $sections = AttributeManager::$sections;
564
565        $params = $this->getCrossWikiParams() + [
566            'filter' => [
567                ParamValidator::PARAM_ISMULTI => true,
568                ParamValidator::PARAM_DEFAULT => 'read|!read',
569                ParamValidator::PARAM_TYPE => [
570                    'read',
571                    '!read',
572                ],
573            ],
574            'prop' => [
575                ParamValidator::PARAM_ISMULTI => true,
576                ParamValidator::PARAM_TYPE => [
577                    'list',
578                    'count',
579                    'seenTime',
580                ],
581                ParamValidator::PARAM_DEFAULT => 'list',
582            ],
583            'sections' => [
584                ParamValidator::PARAM_DEFAULT => implode( '|', $sections ),
585                ParamValidator::PARAM_TYPE => $sections,
586                ParamValidator::PARAM_ISMULTI => true,
587            ],
588            'groupbysection' => [
589                ParamValidator::PARAM_TYPE => 'boolean',
590                ParamValidator::PARAM_DEFAULT => false,
591            ],
592            'format' => [
593                ParamValidator::PARAM_TYPE => [
594                    'model',
595                    'special',
596                    // @deprecated
597                    'flyout',
598                    // @deprecated
599                    'html',
600                ],
601                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
602            ],
603            'limit' => [
604                ParamValidator::PARAM_TYPE => 'limit',
605                ParamValidator::PARAM_DEFAULT => 20,
606                IntegerDef::PARAM_MIN => 1,
607                IntegerDef::PARAM_MAX => ApiBase::LIMIT_SML1,
608                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_SML2,
609            ],
610            'continue' => [
611                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
612            ],
613            'unreadfirst' => [
614                ParamValidator::PARAM_TYPE => 'boolean',
615                ParamValidator::PARAM_DEFAULT => false,
616            ],
617            'titles' => [
618                ParamValidator::PARAM_ISMULTI => true,
619            ],
620            'bundle' => [
621                ParamValidator::PARAM_TYPE => 'boolean',
622                ParamValidator::PARAM_DEFAULT => false,
623            ],
624            'notifiertypes' => [
625                ParamValidator::PARAM_TYPE => $this->allowedNotifierTypes,
626                ParamValidator::PARAM_ISMULTI => true,
627                ParamValidator::PARAM_DEFAULT => 'web',
628            ],
629        ];
630        foreach ( $sections as $section ) {
631            $params[$section . 'continue'] = null;
632            $params[$section . 'unreadfirst'] = [
633                ParamValidator::PARAM_TYPE => 'boolean',
634                ParamValidator::PARAM_DEFAULT => false,
635            ];
636        }
637
638        if ( $this->allowCrossWikiNotifications() ) {
639            $params += [
640                // create "x notifications from y wikis" notification bundle &
641                // include unread counts from other wikis in prop=count results
642                'crosswikisummary' => [
643                    ParamValidator::PARAM_TYPE => 'boolean',
644                    ParamValidator::PARAM_DEFAULT => false,
645                ],
646            ];
647        }
648
649        return $params;
650    }
651
652    /**
653     * @see ApiBase::getExamplesMessages()
654     * @return array
655     */
656    protected function getExamplesMessages() {
657        return [
658            'action=query&meta=notifications'
659                => 'apihelp-query+notifications-example-1',
660            'action=query&meta=notifications&notprop=count&notsections=alert|message&notgroupbysection=1'
661                => 'apihelp-query+notifications-example-2',
662            'action=query&meta=notifications&notnotifiertypes=email'
663                => 'apihelp-query+notifications-example-3',
664        ];
665    }
666
667    public function getHelpUrls() {
668        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/Echo_(Notifications)/API';
669    }
670}