Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ForeignNotifications
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 9
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isEnabledByUser
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCount
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getTimestamp
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 getWikis
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getWikiTimestamp
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 populate
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
132
 getApiEndpoints
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getWikiTitle
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\Notifications;
4
5use MediaWiki\MediaWikiServices;
6use MediaWiki\User\UserIdentity;
7use MediaWiki\Utils\MWTimestamp;
8use MediaWiki\WikiMap\WikiMap;
9
10/**
11 * Caches the result of UnreadWikis::getUnreadCounts() and interprets the results in various useful ways.
12 *
13 * If the user has disabled cross-wiki notifications in their preferences
14 * (see {@see ForeignNotifications::isEnabledByUser}), this class
15 * won't do anything and will behave as if the user has no foreign notifications. For example, getCount() will
16 * return 0. If you need to get foreign notification information for a user even though they may not have
17 * enabled the preference, set $forceEnable=true in the constructor.
18 */
19class ForeignNotifications {
20    /**
21     * @var UserIdentity
22     */
23    protected $user;
24
25    /**
26     * @var bool
27     */
28    protected $enabled = false;
29
30    /**
31     * @var int[] [(str) section => (int) count, ...]
32     */
33    protected $counts = [ AttributeManager::ALERT => 0, AttributeManager::MESSAGE => 0 ];
34
35    /**
36     * @var array[] [(str) section => (string[]) wikis, ...]
37     */
38    protected $wikis = [ AttributeManager::ALERT => [], AttributeManager::MESSAGE => [] ];
39
40    /**
41     * @var array [(str) section => (MWTimestamp) timestamp, ...]
42     */
43    protected $timestamps = [ AttributeManager::ALERT => false, AttributeManager::MESSAGE => false ];
44
45    /**
46     * @var array[] [(str) wiki => [ (str) section => (MWTimestamp) timestamp, ...], ...]
47     */
48    protected $wikiTimestamps = [];
49
50    /**
51     * @var bool
52     */
53    protected $populated = false;
54
55    /**
56     * @param UserIdentity $user
57     * @param bool $forceEnable Ignore the user's preferences and act as if they've enabled cross-wiki notifications
58     */
59    public function __construct( UserIdentity $user, $forceEnable = false ) {
60        $this->user = $user;
61        $this->enabled = $forceEnable || $this->isEnabledByUser();
62    }
63
64    /**
65     * Whether the user has enabled cross-wiki notifications.
66     * @return bool
67     */
68    public function isEnabledByUser() {
69        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
70        return (bool)$userOptionsLookup->getOption( $this->user, 'echo-cross-wiki-notifications' );
71    }
72
73    /**
74     * @param string $section Name of section
75     * @return int
76     */
77    public function getCount( $section = AttributeManager::ALL ) {
78        $this->populate();
79
80        if ( $section === AttributeManager::ALL ) {
81            $count = array_sum( $this->counts );
82        } else {
83            $count = $this->counts[$section] ?? 0;
84        }
85
86        return NotifUser::capNotificationCount( $count );
87    }
88
89    /**
90     * @param string $section Name of section
91     * @return MWTimestamp|false
92     */
93    public function getTimestamp( $section = AttributeManager::ALL ) {
94        $this->populate();
95
96        if ( $section === AttributeManager::ALL ) {
97            $max = false;
98            /** @var MWTimestamp $timestamp */
99            foreach ( $this->timestamps as $timestamp ) {
100                // $timestamp < $max = invert 0
101                // $timestamp > $max = invert 1
102                if ( $timestamp !== false && ( $max === false || $timestamp->diff( $max )->invert === 1 ) ) {
103                    $max = $timestamp;
104                }
105            }
106
107            return $max;
108        }
109
110        return $this->timestamps[$section] ?? false;
111    }
112
113    /**
114     * @param string $section Name of section
115     * @return string[]
116     */
117    public function getWikis( $section = AttributeManager::ALL ) {
118        $this->populate();
119
120        if ( $section === AttributeManager::ALL ) {
121            $all = [];
122            foreach ( $this->wikis as $wikis ) {
123                $all = array_merge( $all, $wikis );
124            }
125
126            return array_unique( $all );
127        }
128
129        return $this->wikis[$section] ?? [];
130    }
131
132    public function getWikiTimestamp( $wiki, $section = AttributeManager::ALL ) {
133        $this->populate();
134        if ( !isset( $this->wikiTimestamps[$wiki] ) ) {
135            return false;
136        }
137        if ( $section === AttributeManager::ALL ) {
138            $max = false;
139            foreach ( $this->wikiTimestamps[$wiki] as $section => $ts ) {
140                // $ts < $max = invert 0
141                // $ts > $max = invert 1
142                if ( $max === false || $ts->diff( $max )->invert === 1 ) {
143                    $max = $ts;
144                }
145            }
146            return $max;
147        }
148        return $this->wikiTimestamps[$wiki][$section] ?? false;
149    }
150
151    protected function populate() {
152        if ( $this->populated ) {
153            return;
154        }
155
156        if ( !$this->enabled ) {
157            return;
158        }
159
160        $unreadWikis = UnreadWikis::newFromUser( $this->user );
161        if ( !$unreadWikis ) {
162            return;
163        }
164        $unreadCounts = $unreadWikis->getUnreadCounts();
165        if ( !$unreadCounts ) {
166            return;
167        }
168
169        foreach ( $unreadCounts as $wiki => $sections ) {
170            // exclude current wiki
171            if ( $wiki === WikiMap::getCurrentWikiId() ) {
172                continue;
173            }
174
175            foreach ( $sections as $section => $data ) {
176                if ( $data['count'] > 0 ) {
177                    $this->counts[$section] += $data['count'];
178                    $this->wikis[$section][] = $wiki;
179
180                    $timestamp = new MWTimestamp( $data['ts'] );
181                    $this->wikiTimestamps[$wiki][$section] = $timestamp;
182
183                    // We need $this->timestamp[$section] to be the max timestamp
184                    // across all wikis.
185                    // $timestamp < $this->timestamps[$section] = invert 0
186                    // $timestamp > $this->timestamps[$section] = invert 1
187                    if (
188                        $this->timestamps[$section] === false ||
189                        $timestamp->diff( $this->timestamps[$section] )->invert === 1
190                    ) {
191                        $this->timestamps[$section] = new MWTimestamp( $data['ts'] );
192                    }
193
194                }
195            }
196        }
197
198        $this->populated = true;
199    }
200
201    /**
202     * @param string[] $wikis
203     * @return array[] [(string) wiki => (array) data]
204     */
205    public static function getApiEndpoints( array $wikis ) {
206        global $wgConf;
207        $wgConf->loadFullData();
208
209        $data = [];
210        foreach ( $wikis as $wiki ) {
211            $siteFromDB = $wgConf->siteFromDB( $wiki );
212            [ $major, $minor ] = $siteFromDB;
213            $server = $wgConf->get( 'wgServer', $wiki, $major, [ 'lang' => $minor, 'site' => $major ] );
214            $scriptPath = $wgConf->get( 'wgScriptPath', $wiki, $major, [ 'lang' => $minor, 'site' => $major ] );
215            $articlePath = $wgConf->get( 'wgArticlePath', $wiki, $major, [ 'lang' => $minor, 'site' => $major ] );
216
217            $data[$wiki] = [
218                'title' => static::getWikiTitle( $wiki, $siteFromDB ),
219                'url' => wfExpandUrl( $server . $scriptPath . '/api.php', PROTO_INTERNAL ),
220                // We need this to link to Special:Notifications page
221                'base' => wfExpandUrl( $server . $articlePath, PROTO_INTERNAL ),
222            ];
223        }
224
225        return $data;
226    }
227
228    /**
229     * @param string $wikiId
230     * @param array|null $siteFromDB $wgConf->siteFromDB( $wikiId ) result
231     * @return mixed|string
232     */
233    protected static function getWikiTitle( $wikiId, array $siteFromDB = null ) {
234        global $wgConf, $wgLang;
235
236        $msg = wfMessage( 'project-localized-name-' . $wikiId );
237        // check if WikimediaMessages localized project names are available
238        if ( $msg->exists() ) {
239            return $msg->text();
240        } else {
241            // Don't fetch [ $site, $langCode ] if known already
242            [ $site, $langCode ] = $siteFromDB ?? $wgConf->siteFromDB( $wikiId );
243
244            // try to fetch site name for this specific wiki, or fallback to the
245            // general project's sitename if there is no override
246            $wikiName = $wgConf->get( 'wgSitename', $wikiId ) ?: $wgConf->get( 'wgSitename', $site );
247            $langName = MediaWikiServices::getInstance()->getLanguageNameUtils()
248                ->getLanguageName( $langCode ?? '', $wgLang->getCode() );
249
250            if ( !$langName ) {
251                // if we can't find a language name (in language-agnostic
252                // project like mediawikiwiki), including the language name
253                // doesn't make much sense
254                return $wikiName;
255            }
256
257            // ... or use generic fallback
258            return wfMessage( 'echo-foreign-wiki-lang', $wikiName, $langName )->text();
259        }
260    }
261}