Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 176
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 176
0.00% covered (danger)
0.00%
0 / 11
2162
0.00% covered (danger)
0.00%
0 / 1
 onBeforeCreateEchoEvent
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
2
 onLoginFormValidErrorMessages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onUserMergeAccountFields
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onCustomEditor
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 onArticleDelete
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 onPageUndelete
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 onTitleMove
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 onContentModelCanBeUsedOn
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
 onEditFilterMergedContent
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
132
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 onGetUserPermissionsErrors
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
4
5namespace MediaWiki\Extension\Newsletter;
6
7use Article;
8use Content;
9use EchoUserLocator;
10use IContextSource;
11use MediaWiki\Content\Hook\ContentModelCanBeUsedOnHook;
12use MediaWiki\Extension\Newsletter\Content\NewsletterContent;
13use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterPresentationModel;
14use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterPublisherAddedPresentationModel;
15use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterPublisherRemovedPresentationModel;
16use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterSubscribedPresentationModel;
17use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterUnsubscribedPresentationModel;
18use MediaWiki\Extension\Newsletter\Notifications\EchoNewsletterUserLocator;
19use MediaWiki\Hook\CustomEditorHook;
20use MediaWiki\Hook\EditFilterMergedContentHook;
21use MediaWiki\Hook\LoginFormValidErrorMessagesHook;
22use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
23use MediaWiki\Hook\TitleMoveHook;
24use MediaWiki\MediaWikiServices;
25use MediaWiki\Page\Hook\ArticleDeleteHook;
26use MediaWiki\Page\Hook\PageUndeleteHook;
27use MediaWiki\Page\ProperPageIdentity;
28use MediaWiki\Permissions\Authority;
29use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
30use MediaWiki\Status\Status;
31use MediaWiki\Title\Title;
32use MediaWiki\User\User;
33use PermissionsError;
34use ReadOnlyError;
35use RuntimeException;
36use SkinTemplate;
37use StatusValue;
38use ThrottledError;
39use WikiPage;
40
41/**
42 * Class to add Hooks used by Newsletter.
43 */
44class 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' => EchoNewsletterPresentationModel::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                [ [ EchoUserLocator::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                [ [ EchoUserLocator::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                [ [ EchoUserLocator::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                [ [ EchoUserLocator::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( $newsletterName );
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}