Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 87 |
|
0.00% |
0 / 9 |
CRAP | |
0.00% |
0 / 1 |
ForeignNotifications | |
0.00% |
0 / 87 |
|
0.00% |
0 / 9 |
1406 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isEnabledByUser | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getCount | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getTimestamp | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
42 | |||
getWikis | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getWikiTimestamp | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
populate | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
132 | |||
getApiEndpoints | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getWikiTitle | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications; |
4 | |
5 | use MediaWiki\MediaWikiServices; |
6 | use MediaWiki\User\UserIdentity; |
7 | use MediaWiki\Utils\MWTimestamp; |
8 | use 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 | */ |
19 | class 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 | } |