Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 129 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ApiEchoUnreadNotificationPages | |
0.00% |
0 / 129 |
|
0.00% |
0 / 7 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
getFromLocal | |
0.00% |
0 / 77 |
|
0.00% |
0 / 1 |
306 | |||
getUnreadNotificationPagesFromForeign | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getAllowedParams | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
getExamplesMessages | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getHelpUrls | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Notifications\Api; |
4 | |
5 | use ApiQuery; |
6 | use ApiQueryBase; |
7 | use ApiUsageException; |
8 | use MediaWiki\Extension\Notifications\DbFactory; |
9 | use MediaWiki\Extension\Notifications\NotifUser; |
10 | use MediaWiki\Extension\Notifications\Services; |
11 | use MediaWiki\Logger\LoggerFactory; |
12 | use MediaWiki\Page\PageRecord; |
13 | use MediaWiki\Page\PageStore; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\Title\TitleFactory; |
16 | use MediaWiki\WikiMap\WikiMap; |
17 | use Wikimedia\ParamValidator\ParamValidator; |
18 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
19 | use Wikimedia\Rdbms\SelectQueryBuilder; |
20 | |
21 | class ApiEchoUnreadNotificationPages extends ApiQueryBase { |
22 | use ApiCrossWiki; |
23 | |
24 | /** |
25 | * @var bool |
26 | */ |
27 | protected $crossWikiSummary = false; |
28 | |
29 | /** |
30 | * @var PageStore |
31 | */ |
32 | private $pageStore; |
33 | |
34 | /** |
35 | * @var TitleFactory |
36 | */ |
37 | private $titleFactory; |
38 | |
39 | /** |
40 | * @param ApiQuery $query |
41 | * @param string $moduleName |
42 | * @param PageStore $pageStore |
43 | * @param TitleFactory $titleFactory |
44 | */ |
45 | public function __construct( $query, $moduleName, PageStore $pageStore, TitleFactory $titleFactory ) { |
46 | parent::__construct( $query, $moduleName, 'unp' ); |
47 | $this->pageStore = $pageStore; |
48 | $this->titleFactory = $titleFactory; |
49 | } |
50 | |
51 | /** |
52 | * @throws ApiUsageException |
53 | */ |
54 | public function execute() { |
55 | // To avoid API warning, register the parameter used to bust browser cache |
56 | $this->getMain()->getVal( '_' ); |
57 | |
58 | if ( !$this->getUser()->isRegistered() ) { |
59 | $this->dieWithError( 'apierror-mustbeloggedin-generic', 'login-required' ); |
60 | } |
61 | |
62 | $params = $this->extractRequestParams(); |
63 | |
64 | $result = []; |
65 | if ( in_array( WikiMap::getCurrentWikiId(), $this->getRequestedWikis() ) ) { |
66 | $result[WikiMap::getCurrentWikiId()] = $this->getFromLocal( $params['limit'], $params['grouppages'] ); |
67 | } |
68 | |
69 | if ( $this->getRequestedForeignWikis() ) { |
70 | $result += $this->getUnreadNotificationPagesFromForeign(); |
71 | } |
72 | |
73 | $apis = $this->getForeignNotifications()->getApiEndpoints( $this->getRequestedWikis() ); |
74 | foreach ( $result as $wiki => $data ) { |
75 | $result[$wiki]['source'] = $apis[$wiki]; |
76 | $result[$wiki]['pages'] = $data['pages'] ?: []; |
77 | } |
78 | |
79 | $this->getResult()->addValue( 'query', $this->getModuleName(), $result ); |
80 | } |
81 | |
82 | /** |
83 | * @param int $limit |
84 | * @param bool $groupPages |
85 | * @return array |
86 | * @phan-return array{pages:array[],totalCount:int} |
87 | */ |
88 | protected function getFromLocal( $limit, $groupPages ) { |
89 | $attributeManager = Services::getInstance()->getAttributeManager(); |
90 | $enabledTypes = $attributeManager->getUserEnabledEvents( $this->getUser(), 'web' ); |
91 | |
92 | $dbr = DbFactory::newFromDefault()->getEchoDb( DB_REPLICA ); |
93 | $queryBuilder = $dbr->newSelectQueryBuilder() |
94 | ->select( [ 'event_page_id', 'count' => 'COUNT(*)' ] ) |
95 | ->from( 'echo_event' ) |
96 | ->join( 'echo_notification', null, 'notification_event = event_id' ) |
97 | ->where( [ |
98 | 'notification_user' => $this->getUser()->getId(), |
99 | 'notification_read_timestamp' => null, |
100 | 'event_deleted' => 0, |
101 | 'event_type' => $enabledTypes, |
102 | ] ) |
103 | ->groupBy( 'event_page_id' ) |
104 | ->caller( __METHOD__ ); |
105 | // If $groupPages is true, we need to fetch all pages and apply the ORDER BY and LIMIT ourselves |
106 | // after grouping. |
107 | if ( !$groupPages ) { |
108 | $queryBuilder |
109 | ->orderBy( 'count', SelectQueryBuilder::SORT_DESC ) |
110 | ->limit( $limit ); |
111 | } |
112 | $rows = $queryBuilder->fetchResultSet(); |
113 | |
114 | $nullCount = 0; |
115 | $pageCounts = []; |
116 | foreach ( $rows as $row ) { |
117 | if ( $row->event_page_id !== null ) { |
118 | $pageCounts[(int)$row->event_page_id] = intval( $row->count ); |
119 | } else { |
120 | $nullCount = intval( $row->count ); |
121 | } |
122 | } |
123 | |
124 | $titles = $this->pageStore |
125 | ->newSelectQueryBuilder() |
126 | ->wherePageIds( array_keys( $pageCounts ) ) |
127 | ->caller( __METHOD__ ) |
128 | ->fetchPageRecords(); |
129 | |
130 | $groupCounts = []; |
131 | /** @var PageRecord $title */ |
132 | foreach ( $titles as $title ) { |
133 | $title = $this->titleFactory->castFromPageIdentity( $title ); |
134 | if ( $groupPages ) { |
135 | // If $title is a talk page, add its count to its subject page's count |
136 | $pageName = $title->getSubjectPage()->getPrefixedText(); |
137 | } else { |
138 | $pageName = $title->getPrefixedText(); |
139 | } |
140 | |
141 | $count = $pageCounts[$title->getArticleID()] ?? 0; |
142 | if ( isset( $groupCounts[$pageName] ) ) { |
143 | $groupCounts[$pageName] += $count; |
144 | } else { |
145 | $groupCounts[$pageName] = $count; |
146 | } |
147 | } |
148 | |
149 | $userPageName = $this->getUser()->getUserPage()->getPrefixedText(); |
150 | if ( $nullCount > 0 && $groupPages ) { |
151 | // Add the count for NULL (not associated with any page) to the count for the user page |
152 | if ( isset( $groupCounts[$userPageName] ) ) { |
153 | $groupCounts[$userPageName] += $nullCount; |
154 | } else { |
155 | $groupCounts[$userPageName] = $nullCount; |
156 | } |
157 | } |
158 | |
159 | arsort( $groupCounts ); |
160 | if ( $groupPages ) { |
161 | $groupCounts = array_slice( $groupCounts, 0, $limit ); |
162 | } |
163 | |
164 | $result = []; |
165 | foreach ( $groupCounts as $pageName => $count ) { |
166 | if ( $groupPages ) { |
167 | $title = Title::newFromText( $pageName ); |
168 | $pages = [ $title->getSubjectPage()->getPrefixedText() ]; |
169 | if ( $title->canHaveTalkPage() ) { |
170 | $pages[] = $title->getTalkPage()->getPrefixedText(); |
171 | } |
172 | if ( $pageName === $userPageName ) { |
173 | $pages[] = null; |
174 | } |
175 | $pageDescription = [ |
176 | 'ns' => $title->getNamespace(), |
177 | 'title' => $title->getPrefixedText(), |
178 | 'unprefixed' => $title->getText(), |
179 | 'pages' => $pages, |
180 | ]; |
181 | } else { |
182 | $pageDescription = [ 'title' => $pageName ]; |
183 | } |
184 | $result[] = $pageDescription + [ |
185 | 'count' => $count, |
186 | ]; |
187 | } |
188 | if ( !$groupPages && $nullCount > 0 ) { |
189 | $result[] = [ |
190 | 'title' => null, |
191 | 'count' => $nullCount, |
192 | ]; |
193 | } |
194 | |
195 | return [ |
196 | 'pages' => $result, |
197 | 'totalCount' => NotifUser::newFromUser( $this->getUser() )->getLocalNotificationCount(), |
198 | ]; |
199 | } |
200 | |
201 | /** |
202 | * @return array[] |
203 | */ |
204 | protected function getUnreadNotificationPagesFromForeign() { |
205 | $result = []; |
206 | foreach ( $this->getFromForeign() as $wiki => $data ) { |
207 | if ( isset( $data['query'][$this->getModuleName()][$wiki] ) ) { |
208 | $result[$wiki] = $data['query'][$this->getModuleName()][$wiki]; |
209 | } else { |
210 | # Usually an error or it is some malformed response |
211 | # T273479 |
212 | LoggerFactory::getInstance( 'Echo' )->warning( |
213 | __METHOD__ . ': Unexpected API response from {wiki}', |
214 | [ |
215 | 'wiki' => $wiki, |
216 | 'data' => $data, |
217 | ] |
218 | ); |
219 | } |
220 | } |
221 | |
222 | return $result; |
223 | } |
224 | |
225 | /** |
226 | * @return array[] |
227 | */ |
228 | public function getAllowedParams() { |
229 | $maxUpdateCount = $this->getConfig()->get( 'EchoMaxUpdateCount' ); |
230 | |
231 | return $this->getCrossWikiParams() + [ |
232 | 'grouppages' => [ |
233 | ParamValidator::PARAM_TYPE => 'boolean', |
234 | ParamValidator::PARAM_DEFAULT => false, |
235 | ], |
236 | 'limit' => [ |
237 | ParamValidator::PARAM_TYPE => 'limit', |
238 | ParamValidator::PARAM_DEFAULT => 10, |
239 | IntegerDef::PARAM_MIN => 1, |
240 | IntegerDef::PARAM_MAX => $maxUpdateCount, |
241 | IntegerDef::PARAM_MAX2 => $maxUpdateCount, |
242 | ], |
243 | // there is no `offset` or `continue` value: the set of possible |
244 | // notifications is small enough to allow fetching all of them at |
245 | // once, and any sort of fetching would be unreliable because |
246 | // they're sorted based on count of notifications, which could |
247 | // change in between requests |
248 | ]; |
249 | } |
250 | |
251 | /** |
252 | * @see ApiBase::getExamplesMessages() |
253 | * @return string[] |
254 | */ |
255 | protected function getExamplesMessages() { |
256 | return [ |
257 | 'action=query&meta=unreadnotificationpages' => 'apihelp-query+unreadnotificationpages-example-1', |
258 | ]; |
259 | } |
260 | |
261 | public function getHelpUrls() { |
262 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/Echo_(Notifications)/API'; |
263 | } |
264 | } |