Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 176 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 176 |
|
0.00% |
0 / 11 |
2162 | |
0.00% |
0 / 1 |
onBeforeCreateEchoEvent | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
2 | |||
onLoginFormValidErrorMessages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onUserMergeAccountFields | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onCustomEditor | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
onArticleDelete | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
onPageUndelete | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
onTitleMove | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onContentModelCanBeUsedOn | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
30 | |||
onEditFilterMergedContent | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
132 | |||
onSkinTemplateNavigation__Universal | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
onGetUserPermissionsErrors | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace MediaWiki\Extension\Newsletter; |
6 | |
7 | use Article; |
8 | use MediaWiki\Content\Content; |
9 | use MediaWiki\Content\Hook\ContentModelCanBeUsedOnHook; |
10 | use MediaWiki\Context\IContextSource; |
11 | use MediaWiki\Extension\Newsletter\Content\NewsletterContent; |
12 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterAnnouncePresentationModel; |
13 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterPublisherAddedPresentationModel; |
14 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterPublisherRemovedPresentationModel; |
15 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterSubscribedPresentationModel; |
16 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterUnsubscribedPresentationModel; |
17 | use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterUserLocator; |
18 | use MediaWiki\Extension\Notifications\UserLocator; |
19 | use MediaWiki\Hook\CustomEditorHook; |
20 | use MediaWiki\Hook\EditFilterMergedContentHook; |
21 | use MediaWiki\Hook\LoginFormValidErrorMessagesHook; |
22 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
23 | use MediaWiki\Hook\TitleMoveHook; |
24 | use MediaWiki\MediaWikiServices; |
25 | use MediaWiki\Page\Hook\ArticleDeleteHook; |
26 | use MediaWiki\Page\Hook\PageUndeleteHook; |
27 | use MediaWiki\Page\ProperPageIdentity; |
28 | use MediaWiki\Permissions\Authority; |
29 | use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook; |
30 | use MediaWiki\Status\Status; |
31 | use MediaWiki\Title\Title; |
32 | use MediaWiki\User\User; |
33 | use PermissionsError; |
34 | use ReadOnlyError; |
35 | use RuntimeException; |
36 | use SkinTemplate; |
37 | use StatusValue; |
38 | use ThrottledError; |
39 | use WikiPage; |
40 | |
41 | /** |
42 | * Class to add Hooks used by Newsletter. |
43 | */ |
44 | class Hooks implements |
45 | LoginFormValidErrorMessagesHook, |
46 | CustomEditorHook, |
47 | ArticleDeleteHook, |
48 | PageUndeleteHook, |
49 | TitleMoveHook, |
50 | ContentModelCanBeUsedOnHook, |
51 | EditFilterMergedContentHook, |
52 | SkinTemplateNavigation__UniversalHook, |
53 | GetUserPermissionsErrorsHook |
54 | { |
55 | |
56 | /** |
57 | * Function to be called before EchoEvent |
58 | * |
59 | * @param array[] &$notifications Echo notifications |
60 | * @param array[] &$notificationCategories Echo notification categories |
61 | */ |
62 | public static function onBeforeCreateEchoEvent( &$notifications, &$notificationCategories ) { |
63 | $notificationCategories['newsletter'] = [ |
64 | 'priority' => 3, |
65 | 'tooltip' => 'echo-pref-tooltip-newsletter', |
66 | ]; |
67 | |
68 | $notifications['newsletter-announce'] = [ |
69 | 'category' => 'newsletter', |
70 | 'section' => 'message', |
71 | 'primary-link' => [ |
72 | 'message' => 'newsletter-notification-link-text-new-issue', |
73 | 'destination' => 'new-issue' |
74 | ], |
75 | 'secondary-link' => [ |
76 | 'message' => 'newsletter-notification-link-text-view-newsletter', |
77 | 'destination' => 'newsletter' |
78 | ], |
79 | 'user-locators' => [ |
80 | [ [ EchoNewsletterUserLocator::class, 'locateNewsletterSubscribedUsers' ] ], |
81 | ], |
82 | 'canNotifyAgent' => true, |
83 | 'presentation-model' => EchoNewsletterAnnouncePresentationModel::class, |
84 | 'title-message' => 'newsletter-notification-title', |
85 | 'title-params' => [ 'newsletter-name', 'title', 'agent', 'user' ], |
86 | 'flyout-message' => 'newsletter-notification-flyout', |
87 | 'flyout-params' => [ 'newsletter-name', 'agent', 'user' ], |
88 | 'payload' => [ 'summary' ], |
89 | 'email-subject-message' => 'newsletter-email-subject', |
90 | 'email-subject-params' => [ 'newsletter-name' ], |
91 | 'email-body-batch-message' => 'newsletter-email-batch-body', |
92 | 'email-body-batch-params' => [ 'newsletter-name', 'agent', 'user' ], |
93 | ]; |
94 | |
95 | $notifications['newsletter-newpublisher'] = [ |
96 | 'category' => 'newsletter', |
97 | 'primary-link' => [ |
98 | 'message' => 'newsletter-notification-link-text-new-publisher', |
99 | 'destination' => 'newsletter' |
100 | ], |
101 | 'user-locators' => [ |
102 | [ [ UserLocator::class, 'locateFromEventExtra' ], [ 'new-publishers-id' ] ] |
103 | ], |
104 | 'presentation-model' => EchoNewsletterPublisherAddedPresentationModel::class, |
105 | 'title-message' => 'newsletter-notification-new-publisher-title', |
106 | 'title-params' => [ 'newsletter-name', 'agent' ], |
107 | 'flyout-message' => 'newsletter-notification-new-publisher-flyout', |
108 | 'flyout-params' => [ 'newsletter-name', 'agent' ], |
109 | ]; |
110 | $notifications['newsletter-delpublisher'] = [ |
111 | 'category' => 'newsletter', |
112 | 'primary-link' => [ |
113 | 'message' => 'newsletter-notification-link-text-del-publisher', |
114 | 'destination' => 'newsletter' |
115 | ], |
116 | 'user-locators' => [ |
117 | [ [ UserLocator::class, 'locateFromEventExtra' ], [ 'del-publishers-id' ] ] |
118 | ], |
119 | 'presentation-model' => EchoNewsletterPublisherRemovedPresentationModel::class, |
120 | 'title-message' => 'newsletter-notification-del-publisher-title', |
121 | 'title-params' => [ 'newsletter-name', 'agent' ], |
122 | 'flyout-message' => 'newsletter-notification-del-publisher-flyout', |
123 | 'flyout-params' => [ 'newsletter-name', 'agent' ], |
124 | ]; |
125 | $notifications['newsletter-subscribed'] = [ |
126 | 'category' => 'newsletter', |
127 | 'primary-link' => [ |
128 | 'message' => 'newsletter-notification-subscribed', |
129 | 'destination' => 'newsletter' |
130 | ], |
131 | 'user-locators' => [ |
132 | [ [ UserLocator::class, 'locateFromEventExtra' ], [ 'new-subscribers-id' ] ] |
133 | ], |
134 | 'presentation-model' => EchoNewsletterSubscribedPresentationModel::class, |
135 | 'title-message' => 'newsletter-notification-subscribed', |
136 | 'title-params' => [ 'newsletter-name' ], |
137 | ]; |
138 | $notifications['newsletter-unsubscribed'] = [ |
139 | 'category' => 'newsletter', |
140 | 'primary-link' => [ |
141 | 'message' => 'newsletter-notification-unsubscribed', |
142 | 'destination' => 'newsletter' |
143 | ], |
144 | 'user-locators' => [ |
145 | [ [ UserLocator::class, 'locateFromEventExtra' ], [ 'removed-subscribers-id' ] ] |
146 | ], |
147 | 'presentation-model' => EchoNewsletterUnsubscribedPresentationModel::class, |
148 | 'title-message' => 'newsletter-notification-unsubscribed', |
149 | 'title-params' => [ 'newsletter-name' ], |
150 | ]; |
151 | } |
152 | |
153 | /** |
154 | * Allows to add our own error message to LoginForm |
155 | * |
156 | * @param array &$messages |
157 | */ |
158 | public function onLoginFormValidErrorMessages( array &$messages ) { |
159 | // on Special:Newsletter/id/subscribe |
160 | $messages[] = 'newsletter-subscribe-loginrequired'; |
161 | } |
162 | |
163 | /** |
164 | * Tables that Extension:UserMerge needs to update |
165 | * |
166 | * @param array &$updateFields |
167 | */ |
168 | public static function onUserMergeAccountFields( array &$updateFields ) { |
169 | $updateFields[] = [ 'nl_publishers', 'nlp_publisher_id' ]; |
170 | $updateFields[] = [ 'nl_subscriptions', 'nls_subscriber_id' ]; |
171 | } |
172 | |
173 | /** |
174 | * @param Article $article |
175 | * @param User $user |
176 | * @return bool |
177 | * @throws ReadOnlyError |
178 | */ |
179 | public function onCustomEditor( $article, $user ) { |
180 | if ( !$article->getTitle()->inNamespace( NS_NEWSLETTER ) ) { |
181 | return true; |
182 | } |
183 | $newsletter = Newsletter::newFromName( $article->getTitle()->getText() ); |
184 | if ( $newsletter ) { |
185 | // A newsletter exists in that title, lets redirect to manage page |
186 | $editPage = new NewsletterEditPage( $article->getContext(), $newsletter ); |
187 | $editPage->edit(); |
188 | return false; |
189 | } |
190 | |
191 | $editPage = new NewsletterEditPage( $article->getContext() ); |
192 | $editPage->edit(); |
193 | return false; |
194 | } |
195 | |
196 | /** |
197 | * @param WikiPage $wikiPage |
198 | * @param User $user |
199 | * @param string &$reason |
200 | * @param string &$error |
201 | * @param Status &$status |
202 | * @param bool $suppress |
203 | * @throws PermissionsError |
204 | */ |
205 | public function onArticleDelete( |
206 | WikiPage $wikiPage, |
207 | User $user, |
208 | &$reason, |
209 | &$error, |
210 | Status &$status, |
211 | $suppress |
212 | ) { |
213 | if ( !$wikiPage->getTitle()->inNamespace( NS_NEWSLETTER ) ) { |
214 | return; |
215 | } |
216 | $newsletter = Newsletter::newFromName( $wikiPage->getTitle()->getText() ); |
217 | if ( $newsletter ) { |
218 | if ( !$newsletter->canDelete( $user ) ) { |
219 | throw new PermissionsError( 'newsletter-delete' ); |
220 | } |
221 | NewsletterStore::getDefaultInstance()->deleteNewsletter( $newsletter ); |
222 | } |
223 | } |
224 | |
225 | /** |
226 | * @param ProperPageIdentity $page |
227 | * @param Authority $performer |
228 | * @param string $reason |
229 | * @param bool $unsuppress |
230 | * @param array $timestamps |
231 | * @param array $fileVersions |
232 | * @param StatusValue $status |
233 | * @return bool|void |
234 | */ |
235 | public function onPageUndelete( |
236 | ProperPageIdentity $page, |
237 | Authority $performer, |
238 | string $reason, |
239 | bool $unsuppress, |
240 | array $timestamps, |
241 | array $fileVersions, |
242 | StatusValue $status |
243 | ) { |
244 | $title = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $page )->getTitle(); |
245 | if ( !$title->inNamespace( NS_NEWSLETTER ) ) { |
246 | return; |
247 | } |
248 | $newsletterName = $title->getText(); |
249 | $newsletter = Newsletter::newFromName( $newsletterName, false ); |
250 | if ( $newsletter ) { |
251 | if ( !$newsletter->canRestore( $performer ) ) { |
252 | $status->merge( User::newFatalPermissionDeniedStatus( 'newsletter-restore' ) ); |
253 | return false; |
254 | } |
255 | $store = NewsletterStore::getDefaultInstance(); |
256 | $rows = $store->newsletterExistsForMainPage( $newsletter->getPageId() ); |
257 | foreach ( $rows as $row ) { |
258 | if ( (int)$row->nl_main_page_id === $newsletter->getPageId() && (int)$row->nl_active === 1 ) { |
259 | $status->fatal( 'newsletter-mainpage-in-use' ); |
260 | return false; |
261 | } |
262 | } |
263 | $store->restoreNewsletter( $newsletter ); |
264 | } elseif ( !$title->exists() ) { |
265 | // If the title exists, then there's no reason to block the undeletion |
266 | // whatever you are doing is probably a bad idea, but won't cause any inconsistencies |
267 | // since it will attach the disconnected revisions to the existing page |
268 | $status->fatal( 'newsletter-orphan-revisions' ); |
269 | return false; |
270 | } |
271 | } |
272 | |
273 | /** |
274 | * @param Title $title |
275 | * @param Title $newtitle |
276 | * @param User $user |
277 | * @param string $reason |
278 | * @param Status &$status |
279 | */ |
280 | public function onTitleMove( |
281 | Title $title, |
282 | Title $newtitle, |
283 | User $user, |
284 | $reason, |
285 | Status &$status |
286 | ) { |
287 | if ( $newtitle->inNamespace( NS_NEWSLETTER ) ) { |
288 | $newsletter = Newsletter::newFromName( $title->getText() ); |
289 | if ( $newsletter ) { |
290 | NewsletterStore::getDefaultInstance()->updateName( $newsletter->getId(), $newtitle->getText() ); |
291 | } else { |
292 | throw new RuntimeException( 'Cannot find newsletter with name \"' . $title->getText() . '\"' ); |
293 | } |
294 | } |
295 | } |
296 | |
297 | /** |
298 | * Enforce the invariant that all pages in the Newsletter namespace |
299 | * correspond to an actual newsletter in the database by preventing |
300 | * any other content models from being used there. |
301 | * @param string $contentModel ID of the content model in question |
302 | * @param Title $title the Title in question. |
303 | * @param bool &$ok Output parameter, whether it is OK to use $contentModel on $title. |
304 | */ |
305 | public function onContentModelCanBeUsedOn( $contentModel, $title, &$ok ) { |
306 | if ( $title->inNamespace( NS_NEWSLETTER ) && $contentModel != 'NewsletterContent' ) { |
307 | $ok = false; |
308 | } elseif ( !$title->inNamespace( NS_NEWSLETTER ) && $contentModel == 'NewsletterContent' ) { |
309 | $ok = false; |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * @param IContextSource $context object implementing the IContextSource interface. |
315 | * @param Content $content content of the edit box, as a Content object. |
316 | * @param Status $status Status object to represent errors, etc. |
317 | * @param string $summary Edit summary for page |
318 | * @param User $user the User object representing the user who is performing the edit. |
319 | * @param bool $minoredit whether the edit was marked as minor by the user. |
320 | * @return bool |
321 | * @throws ThrottledError |
322 | */ |
323 | public function onEditFilterMergedContent( |
324 | IContextSource $context, |
325 | Content $content, |
326 | Status $status, |
327 | $summary, |
328 | User $user, |
329 | $minoredit |
330 | ) { |
331 | if ( !$context->getTitle()->inNamespace( NS_NEWSLETTER ) ) { |
332 | return true; |
333 | } |
334 | if ( !$context->getTitle()->hasContentModel( 'NewsletterContent' ) || |
335 | ( !$content instanceof NewsletterContent ) |
336 | ) { |
337 | return true; |
338 | } |
339 | if ( $user->pingLimiter( 'newsletter' ) ) { |
340 | // Default user access level for creating a newsletter is quite low |
341 | // so add a throttle here to prevent abuse (eg. mass vandalism spree) |
342 | throw new ThrottledError; |
343 | } |
344 | $newsletter = Newsletter::newFromName( $context->getTitle()->getText() ); |
345 | |
346 | // Validate API Edit parameters |
347 | $formData = [ |
348 | 'Name' => $context->getTitle()->getText(), |
349 | 'Description' => $content->getDescription(), |
350 | 'MainPage' => $content->getMainPage(), |
351 | ]; |
352 | $validator = new NewsletterValidator( $formData ); |
353 | $validation = $validator->validate( !$newsletter ); |
354 | if ( !$validation->isGood() ) { |
355 | $status->merge( $validation ); |
356 | // Invalid input was entered |
357 | return false; |
358 | } |
359 | $mainPageId = $content->getMainPage()->getArticleID(); |
360 | $store = NewsletterStore::getDefaultInstance(); |
361 | if ( !$newsletter || $newsletter->getPageId() !== $mainPageId ) { |
362 | $rows = $store->newsletterExistsForMainPage( $mainPageId ); |
363 | foreach ( $rows as $row ) { |
364 | if ( (int)$row->nl_main_page_id === $mainPageId && (int)$row->nl_active === 1 ) { |
365 | $status->fatal( 'newsletter-mainpage-in-use' ); |
366 | return false; |
367 | } |
368 | } |
369 | } |
370 | |
371 | return true; |
372 | } |
373 | |
374 | /** |
375 | * Hide the View Source tab in the Newsletter namespace for users who do not have any |
376 | * view permission ('newsletter-*') |
377 | * |
378 | * @param SkinTemplate $skinTemplate The skin template on which the UI is built. |
379 | * @param array &$links Navigation links. |
380 | */ |
381 | public function onSkinTemplateNavigation__Universal( $skinTemplate, &$links ): void { |
382 | if ( $skinTemplate->getTitle()->inNamespace( NS_NEWSLETTER ) ) { |
383 | unset( $links['views']['viewsource'] ); |
384 | } |
385 | } |
386 | |
387 | /** |
388 | * @param Title $title The title that permissions are being checked for |
389 | * @param User $user The User object representing the user who is attempting to perform the action |
390 | * @param string $action The action attempting to be performed |
391 | * @param string &$result Output parameter, set to a string to signify that the action isn't allowed |
392 | * @return bool |
393 | */ |
394 | public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) { |
395 | if ( !$title->inNamespace( NS_NEWSLETTER ) ) { |
396 | return true; |
397 | } |
398 | if ( $action === 'edit' ) { |
399 | if ( $title->exists() ) { |
400 | $newsletter = Newsletter::newFromName( $title->getText() ); |
401 | if ( !$newsletter->canManage( $user ) ) { |
402 | // This case can only trigger when using the API - the UI won't display an edit form at all |
403 | $result = "newsletter-api-error-nopermissions"; |
404 | return false; |
405 | } |
406 | } |
407 | } elseif ( $action === 'create' ) { |
408 | $permissionManager = MediaWikiServices::getInstance()->getPermissionManager(); |
409 | if ( !$permissionManager->userHasRight( $user, 'newsletter-create' ) ) { |
410 | // This case can only trigger when using the API - the UI will display the standard |
411 | // "The action you have requested is limited to users in the group <groupnames>" error |
412 | $result = "newsletter-api-error-nocreate"; |
413 | return false; |
414 | } |
415 | } |
416 | } |
417 | } |