Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 115
0.00% covered (danger)
0.00%
0 / 16
1190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getDiscoveryMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isBetaDiscoveryMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isPublicDiscoveryMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 purgeStories
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 deleteStories
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getDiscoverBundleData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArticleSectionTitle
0.00% covered (danger)
0.00%
0 / 6
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
 onPageSaveComplete
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 onPageDeleteComplete
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 onPageUndeleteComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onParserCacheSaveComplete
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 onArticlePurge
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onActionModifyFormFields
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\Wikistories;
4
5use Article;
6use ManualLogEntry;
7use MediaWiki\Config\Config;
8use MediaWiki\Deferred\DeferredUpdates;
9use MediaWiki\Extension\Wikistories\Jobs\ArticleChangedJob;
10use MediaWiki\Hook\ActionModifyFormFieldsHook;
11use MediaWiki\Hook\LoginFormValidErrorMessagesHook;
12use MediaWiki\Hook\ParserCacheSaveCompleteHook;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Page\Hook\ArticlePurgeHook;
15use MediaWiki\Page\Hook\PageDeleteCompleteHook;
16use MediaWiki\Page\Hook\PageUndeleteCompleteHook;
17use MediaWiki\Page\ProperPageIdentity;
18use MediaWiki\Parser\ParserOutput;
19use MediaWiki\Permissions\Authority;
20use MediaWiki\Preferences\Hook\GetPreferencesHook;
21use MediaWiki\Revision\RevisionRecord;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\Storage\EditResult;
24use MediaWiki\Storage\Hook\PageSaveCompleteHook;
25use MediaWiki\Title\Title;
26use MediaWiki\User\User;
27use MediaWiki\User\UserIdentity;
28use ParserCache;
29use ParserOptions;
30use RequestContext;
31use WikiPage;
32
33class Hooks implements
34    LoginFormValidErrorMessagesHook,
35    PageSaveCompleteHook,
36    PageDeleteCompleteHook,
37    PageUndeleteCompleteHook,
38    GetPreferencesHook,
39    ParserCacheSaveCompleteHook,
40    ArticlePurgeHook,
41    ActionModifyFormFieldsHook
42{
43
44    public const WIKISTORIES_PREF_SHOW_DISCOVERY = 'wikistories-pref-showdiscovery';
45
46    private const WIKISTORIES_MODE_BETA = 'beta';
47
48    private const WIKISTORIES_MODE_PUBLIC = 'public';
49
50    private const WIKISTORIES_PREF_VIEWER_TEXTSIZE = 'wikistories-pref-viewertextsize';
51
52    /** @var Config */
53    private $mainConfig;
54
55    /**
56     * @param Config $mainConfig
57     */
58    public function __construct( Config $mainConfig ) {
59        $this->mainConfig = $mainConfig;
60    }
61
62    /**
63     * @param User $user
64     * @param array &$preferences
65     */
66    public function onGetPreferences( $user, &$preferences ) {
67        if ( self::isPublicDiscoveryMode( $this->mainConfig ) ) {
68            $preferences[ self::WIKISTORIES_PREF_SHOW_DISCOVERY ] = [
69                'section' => 'rendering/wikistories',
70                'label-message' => 'wikistories-pref-showdiscovery-message',
71                'help-message' => 'wikistories-pref-showdiscovery-help-message',
72                'type' => 'toggle',
73            ];
74        }
75        $preferences[ self::WIKISTORIES_PREF_VIEWER_TEXTSIZE ] = [
76            'type' => 'api',
77        ];
78    }
79
80    /**
81     * @param Config $config
82     * @return mixed
83     */
84    private static function getDiscoveryMode( Config $config ) {
85        return $config->get( 'WikistoriesDiscoveryMode' );
86    }
87
88    /**
89     * @param Config $config
90     * @return bool
91     */
92    public static function isBetaDiscoveryMode( Config $config ): bool {
93        return self::getDiscoveryMode( $config ) === self::WIKISTORIES_MODE_BETA;
94    }
95
96    /**
97     * @param Config $config
98     * @return bool
99     */
100    public static function isPublicDiscoveryMode( Config $config ): bool {
101        return self::getDiscoveryMode( $config ) === self::WIKISTORIES_MODE_PUBLIC;
102    }
103
104    /**
105     * @param ProperPageIdentity $page
106     */
107    private static function purgeStories( ProperPageIdentity $page ) {
108        $services = MediaWikiServices::getInstance();
109        /** @var PageLinksSearch $pageLinksSearch */
110        $pageLinksSearch = $services->get( 'Wikistories.PageLinksSearch' );
111        $wikiPageFactory = $services->getWikiPageFactory();
112        $storiesId = $pageLinksSearch->getPageLinks( $page->getDBkey(), 99 );
113        foreach ( $storiesId as $storyId ) {
114            $page = $wikiPageFactory->newFromID( $storyId );
115            $page->doPurge();
116        }
117    }
118
119    /**
120     * @param ProperPageIdentity $page
121     * @param Authority $deleter
122     */
123    private static function deleteStories( ProperPageIdentity $page, Authority $deleter ) {
124        $request = RequestContext::getMain()->getRequest();
125        $services = MediaWikiServices::getInstance();
126        /** @var PageLinksSearch $pageLinksSearch */
127        $pageLinksSearch = $services->get( 'Wikistories.PageLinksSearch' );
128        $wikiPageFactory = $services->getWikiPageFactory();
129        $deletePageFactory = $services->getDeletePageFactory();
130        $storiesId = $pageLinksSearch->getPageLinks( $page->getDBkey(), 99 );
131        foreach ( $storiesId as $storyId ) {
132            $page = $wikiPageFactory->newFromID( $storyId );
133            $deletePage = $deletePageFactory->newDeletePage(
134                $page,
135                $deleter
136            );
137            $deletePage
138                ->setSuppress( $request->getBool( 'wpSuppress' ) )
139                ->deleteIfAllowed( $request->getText( 'wpReason' ) );
140        }
141    }
142
143    /**
144     * @return array Data used by the 'discover' module
145     */
146    public static function getDiscoverBundleData(): array {
147        return [ 'storyBuilder' => SpecialPage::getTitleValueFor( 'StoryBuilder' )->getText() ];
148    }
149
150    /**
151     * @return array Data used by the 'builder' module to get title translation
152     */
153    public static function getArticleSectionTitle(): array {
154        return [
155            'See_also' => [
156                'en' => 'See_also',
157                'id' => 'Lihat_pula'
158            ]
159        ];
160    }
161
162    /**
163     * Register a message to make sure Special:StoryBuilder can redirect
164     * to the login page when the user is logged out.
165     *
166     * @param string[] &$messages List of messages valid on login screen
167     */
168    public function onLoginFormValidErrorMessages( array &$messages ) {
169        $messages[] = 'wikistories-specialstorybuilder-mustbeloggedin';
170    }
171
172    /**
173     * When editing a story with the form, it is possible to change the 'Related article'
174     * to change which article the story will be shown on. The link to the new article will
175     * be done automatically with the page links but it will still show on the previous
176     * article because of the long-live stories cache.
177     *
178     * This hook invalidates the stories cache for the old article.
179     *
180     * @param WikiPage $wikiPage
181     * @param UserIdentity $user
182     * @param string $summary
183     * @param int $flags
184     * @param RevisionRecord $revisionRecord
185     * @param EditResult $editResult
186     */
187    public function onPageSaveComplete(
188        $wikiPage,
189        $user,
190        $summary,
191        $flags,
192        $revisionRecord,
193        $editResult
194    ) {
195        if ( $wikiPage->getNamespace() !== NS_STORY ) {
196            return;
197        }
198
199        if ( $wikiPage->getContentModel() !== 'story' ) {
200            return;
201        }
202
203        DeferredUpdates::addCallableUpdate( static function () use ( $wikiPage, $revisionRecord ) {
204            $services = MediaWikiServices::getInstance();
205            /** @var StoriesCache $cache */
206            $cache = $services->get( 'Wikistories.Cache' );
207            /** @var StoryContent $story */
208            $story = $revisionRecord->getContent( 'main' );
209            '@phan-var StoryContent $story';
210            $articleTitle = $story->getArticleTitle();
211            if ( $articleTitle ) {
212                $cache->invalidateForArticle( $articleTitle->getId() );
213            }
214            $cache->invalidateStory( $wikiPage->getId() );
215        } );
216    }
217
218    /**
219     * Do purge stories when article is deleted
220     * Invalidate stories cache for the related article
221     *
222     * @param ProperPageIdentity $page
223     * @param Authority $deleter
224     * @param string $reason
225     * @param int $pageID
226     * @param RevisionRecord $deletedRev
227     * @param ManualLogEntry $logEntry
228     * @param int $archivedRevisionCount
229     */
230    public function onPageDeleteComplete(
231        ProperPageIdentity $page,
232        Authority $deleter,
233        string $reason,
234        int $pageID,
235        RevisionRecord $deletedRev,
236        ManualLogEntry $logEntry,
237        int $archivedRevisionCount
238    ) {
239        // NS_MAIN deletion
240        if ( $page->getNamespace() === NS_MAIN ) {
241            $deleteStories = RequestContext::getMain()->getRequest()->getBool( 'wpDeleteStory' );
242            DeferredUpdates::addCallableUpdate( static function () use ( $page, $deleter, $deleteStories ) {
243                if ( $deleteStories ) {
244                    self::deleteStories( $page, $deleter );
245                } else {
246                    self::purgeStories( $page );
247                }
248            } );
249            return;
250        }
251
252        // NS_STORY deletion
253        if ( $page->getNamespace() !== NS_STORY ) {
254            return;
255        }
256
257        $story = $deletedRev->getContent( 'main' );
258        if ( !( $story instanceof StoryContent ) ) {
259            return;
260        }
261
262        DeferredUpdates::addCallableUpdate( static function () use ( $pageID ) {
263            $services = MediaWikiServices::getInstance();
264            /** @var StoriesCache $cache */
265            $cache = $services->get( 'Wikistories.Cache' );
266            $cache->invalidateForArticle( $pageID );
267        } );
268    }
269
270    /**
271     * Do purge stories when article is undeleted
272     *
273     * @param ProperPageIdentity $page
274     * @param Authority $restorer
275     * @param string $reason
276     * @param RevisionRecord $restoredRev
277     * @param ManualLogEntry $logEntry
278     * @param int $restoredRevisionCount
279     * @param bool $created
280     * @param array $restoredPageIds
281     */
282    public function onPageUndeleteComplete(
283        ProperPageIdentity $page,
284        Authority $restorer,
285        string $reason,
286        RevisionRecord $restoredRev,
287        ManualLogEntry $logEntry,
288        int $restoredRevisionCount,
289        bool $created,
290        array $restoredPageIds
291    ): void {
292        // NS_MAIN deletion
293        if ( $page->getNamespace() === NS_MAIN ) {
294            DeferredUpdates::addCallableUpdate( static function () use ( $page ) {
295                self::purgeStories( $page );
296            } );
297            return;
298        }
299    }
300
301    /**
302     * @param ParserCache $parserCache
303     * @param ParserOutput $parserOutput
304     * @param Title $title
305     * @param ParserOptions $parserOptions
306     * @param int $revId
307     */
308    public function onParserCacheSaveComplete(
309        $parserCache,
310        $parserOutput,
311        $title,
312        $parserOptions,
313        $revId
314    ) {
315        if ( $title->getNamespace() !== NS_MAIN ) {
316            return;
317        }
318
319        if ( $parserOptions->getRenderReason() !== 'edit-page' ) {
320            // Don't want to trigger story outdated verification for any other reason
321            return;
322        }
323
324        DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
325            /** @var PageLinksSearch $pageLinkSearch */
326            $pageLinkSearch = MediaWikiServices::getInstance()->get( 'Wikistories.PageLinksSearch' );
327            $links = $pageLinkSearch->getPageLinks( $title->getDBkey(), 1 );
328            if ( count( $links ) === 0 ) {
329                return;
330            }
331
332            $job = ArticleChangedJob::newSpec( $title->getId() );
333            MediaWikiServices::getInstance()->getJobQueueGroup()->push( $job );
334        } );
335    }
336
337    /**
338     * @param WikiPage $wikiPage
339     * @return void
340     */
341    public function onArticlePurge( $wikiPage ) {
342        if ( $wikiPage->getNamespace() !== NS_STORY ) {
343            return;
344        }
345
346        $services = MediaWikiServices::getInstance();
347        /** @var StoriesCache $cache */
348        $cache = $services->get( 'Wikistories.Cache' );
349        $cache->invalidateStory( $wikiPage->getId() );
350    }
351
352    /**
353     * @param string $name
354     * @param array &$fields
355     * @param Article $article
356     */
357    public function onActionModifyFormFields(
358        $name,
359        &$fields,
360        $article
361    ) {
362        // skip when not delete action and not an article
363        if ( $name !== 'delete' || $article->getPage()->getNamespace() !== NS_MAIN ) {
364            return;
365        }
366
367        // skip when no stories found in this article
368        $pageLinkSearch = MediaWikiServices::getInstance()->get( 'Wikistories.PageLinksSearch' );
369        $title = $article->getPage()->getTitle()->getDBkey();
370        $links = $pageLinkSearch->getPageLinks( $title, 1 );
371        if ( count( $links ) === 0 ) {
372            return;
373        }
374
375        // Add DeleteStory Field before ConfirmB
376        // @todo Add Unit Test to prevent UI break when DeleteAction.php change
377        $confirmBField = $fields[ 'ConfirmB' ];
378        unset( $fields[ 'ConfirmB' ] );
379        $fields[ 'DeleteStory' ] = [
380            'type' => 'check',
381            'id' => 'wpDeleteStory',
382            'default' => true,
383            'tabIndex' => $confirmBField[ 'tabindex' ] + 1,
384            'label-message' => 'deletepage-deletestory'
385        ];
386        $fields[ 'ConfirmB' ] = $confirmBField;
387    }
388}