Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.46% covered (danger)
38.46%
20 / 52
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CampaignHooks
38.46% covered (danger)
38.46%
20 / 52
50.00% covered (danger)
50.00%
5 / 10
183.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 onPageDeleteComplete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 onEditFilterMergedContent
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
7.19
 onLinksUpdateComplete
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 doCampaignUpdate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 onPageDelete
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 onMovePageIsValidMove
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isGlobalConfigAnchor
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isMagicUser
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\MediaUploader\Hooks;
4
5use Content;
6use DeferredUpdates;
7use IContextSource;
8use LinksUpdate;
9use ManualLogEntry;
10use MediaWiki\Extension\MediaUploader\Campaign\CampaignContent;
11use MediaWiki\Extension\MediaUploader\Campaign\CampaignStore;
12use MediaWiki\Extension\MediaUploader\Config\ConfigCacheInvalidator;
13use MediaWiki\Extension\MediaUploader\MediaUploaderServices;
14use MediaWiki\Hook\EditFilterMergedContentHook;
15use MediaWiki\Hook\LinksUpdateCompleteHook;
16use MediaWiki\Hook\MovePageIsValidMoveHook;
17use MediaWiki\Linker\LinkTarget;
18use MediaWiki\Page\Hook\PageDeleteCompleteHook;
19use MediaWiki\Page\Hook\PageDeleteHook;
20use MediaWiki\Page\ProperPageIdentity;
21use MediaWiki\Permissions\Authority;
22use MediaWiki\Revision\RevisionRecord;
23use MediaWiki\Storage\EditResult;
24use MediaWiki\Storage\Hook\PageSaveCompleteHook;
25use MediaWiki\User\UserIdentity;
26use Status;
27use StatusValue;
28use Title;
29use TitleValue;
30use User;
31use Wikimedia\Assert\PreconditionException;
32use WikiPage;
33
34/**
35 * Hooks related to handling events happening on pages in the Campaign: namespace.
36 */
37class CampaignHooks implements
38    EditFilterMergedContentHook,
39    LinksUpdateCompleteHook,
40    MovePageIsValidMoveHook,
41    PageDeleteCompleteHook,
42    PageDeleteHook,
43    PageSaveCompleteHook
44{
45
46    /** @var CampaignStore */
47    private $campaignStore;
48
49    /** @var ConfigCacheInvalidator */
50    private $cacheInvalidator;
51
52    /**
53     * @param CampaignStore $campaignStore
54     * @param ConfigCacheInvalidator $cacheInvalidator
55     */
56    public function __construct(
57        CampaignStore $campaignStore,
58        ConfigCacheInvalidator $cacheInvalidator
59    ) {
60        $this->campaignStore = $campaignStore;
61        $this->cacheInvalidator = $cacheInvalidator;
62    }
63
64    /**
65     * Deletes entries from mu_campaign table when a Campaign is deleted
66     *
67     * @param ProperPageIdentity $page
68     * @param Authority $deleter
69     * @param string $reason
70     * @param int $pageID
71     * @param RevisionRecord $deletedRev
72     * @param ManualLogEntry $logEntry
73     * @param int $archivedRevisionCount
74     *
75     * @return true
76     */
77    public function onPageDeleteComplete(
78        ProperPageIdentity $page, Authority $deleter, string $reason, int $pageID,
79        RevisionRecord $deletedRev, ManualLogEntry $logEntry, int $archivedRevisionCount
80    ): bool {
81        if ( $page->getNamespace() !== NS_CAMPAIGN ) {
82            return true;
83        }
84
85        $this->campaignStore->deleteCampaignByPageId( $pageID );
86        return true;
87    }
88
89    /**
90     * Validates that the revised contents of a campaign are valid YAML.
91     * If not valid, rejects edit with error message.
92     *
93     * @param IContextSource $context
94     * @param Content $content
95     * @param Status $status
96     * @param string $summary
97     * @param User $user
98     * @param bool $minoredit
99     *
100     * @return bool
101     */
102    public function onEditFilterMergedContent(
103        IContextSource $context,
104        Content $content,
105        Status $status,
106        $summary,
107        User $user,
108        $minoredit
109    ): bool {
110        if ( !$context->getTitle()->inNamespace( NS_CAMPAIGN )
111            || !$content instanceof CampaignContent
112        ) {
113            return true;
114        }
115
116        if ( $this->isGlobalConfigAnchor( $context->getTitle() ) ) {
117            // There's no need to validate the anchor's contents, it doesn't
118            // matter anyway.
119            return true;
120        }
121
122        if ( MediaUploaderServices::isSystemUser( $user ) ) {
123            return true;
124        }
125
126        $status->merge( $content->getValidationStatus() );
127
128        return $status->isOK();
129    }
130
131    /**
132     * Invalidates the cache for a campaign when any of its dependents are edited. The
133     * 'dependents' are tracked by entries in the templatelinks table, which are inserted
134     * by CampaignContent.
135     *
136     * This is usually run via the Job Queue mechanism.
137     *
138     * @param LinksUpdate $linksUpdate
139     * @param mixed $ticket
140     *
141     * @return bool
142     */
143    public function onLinksUpdateComplete( $linksUpdate, $ticket ): bool {
144        if ( !$linksUpdate->getTitle()->inNamespace( NS_CAMPAIGN ) ) {
145            return true;
146        }
147
148        // Invalidate global config cache.
149        if ( $this->isGlobalConfigAnchor( $linksUpdate->getTitle() ) ) {
150            // Ignore edits by MediaUploader itself.
151            // The cache was invalidated recently anyway.
152            if ( !$this->isMagicUser( $linksUpdate->getTriggeringUser() ) ) {
153                $this->cacheInvalidator->invalidate();
154            }
155            return true;
156        }
157
158        $this->cacheInvalidator->invalidate( $linksUpdate->getTitle()->getDBkey() );
159
160        return true;
161    }
162
163    /**
164     * Sets up appropriate entries in the uc_campaigns table for each Campaign
165     * Acts everytime a page in the NS_CAMPAIGN namespace is saved
166     *
167     * The real update is done in doCampaignUpdate
168     *
169     * @param WikiPage $wikiPage
170     * @param UserIdentity $userIdentity
171     * @param string $summary
172     * @param int $flags
173     * @param RevisionRecord $revisionRecord
174     * @param EditResult $editResult
175     *
176     * @return bool
177     */
178    public function onPageSaveComplete(
179        $wikiPage, $userIdentity, $summary, $flags, $revisionRecord, $editResult
180    ): bool {
181        $content = $wikiPage->getContent();
182        if ( !$content instanceof CampaignContent
183            || $this->isGlobalConfigAnchor( $wikiPage->getTitle() )
184        ) {
185            return true;
186        }
187
188        DeferredUpdates::addCallableUpdate(
189            function () use ( $wikiPage, $content ) {
190                $this->doCampaignUpdate( $wikiPage, $content );
191            }
192        );
193
194        return true;
195    }
196
197    /**
198     * Performs the actual campaign data update after the campaign page is saved.
199     *
200     * @param WikiPage $wikiPage
201     * @param CampaignContent $content
202     */
203    public function doCampaignUpdate( WikiPage $wikiPage, CampaignContent $content ): void {
204        $campaignRecord = $content->newCampaignRecord( $wikiPage, $wikiPage->getId() );
205        $this->campaignStore->upsertCampaign( $campaignRecord );
206    }
207
208    /**
209     * Prevent the global config anchor from being deleted.
210     *
211     * @param ProperPageIdentity $page
212     * @param Authority $deleter
213     * @param string $reason
214     * @param StatusValue $status
215     * @param bool $suppress
216     *
217     * @return bool
218     */
219    public function onPageDelete(
220        ProperPageIdentity $page, Authority $deleter, string $reason, StatusValue $status, bool $suppress
221    ): bool {
222        if ( $this->isGlobalConfigAnchor( TitleValue::newFromPage( $page ) ) ) {
223            $status->fatal( 'mediauploader-global-config-anchor' );
224            return false;
225        }
226        return true;
227    }
228
229    /**
230     * Prevent the global config anchor from being moved.
231     *
232     * @param Title $oldTitle
233     * @param Title $newTitle
234     * @param Status $status
235     *
236     * @return bool
237     */
238    public function onMovePageIsValidMove( $oldTitle, $newTitle, $status ): bool {
239        if ( $this->isGlobalConfigAnchor( $oldTitle ) ||
240            $this->isGlobalConfigAnchor( $newTitle )
241        ) {
242            $status->fatal( 'mediauploader-global-config-anchor' );
243        }
244        return true;
245    }
246
247    /**
248     * Checks whether $linkTarget is of the global config anchor page.
249     *
250     * @param LinkTarget $linkTarget
251     *
252     * @return bool
253     */
254    private function isGlobalConfigAnchor( LinkTarget $linkTarget ): bool {
255        return $linkTarget->isSameLinkAs(
256            CampaignContent::getGlobalConfigAnchorLinkTarget()
257        );
258    }
259
260    /**
261     * Checks whether $identity is of the "magic" built-in MediaUploader user.
262     *
263     * @param UserIdentity|null $identity
264     *
265     * @return bool
266     */
267    private function isMagicUser( ?UserIdentity $identity ): bool {
268        if ( $identity === null ) {
269            return false;
270        }
271        try {
272            $identity->assertWiki( UserIdentity::LOCAL );
273        } catch ( PreconditionException $ex ) {
274            return false;
275        }
276        return $identity->isRegistered() && $identity->getName() === 'MediaUploader';
277    }
278}