Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
StoriesEventIngress
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 6
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 handlePageRevisionUpdatedEvent
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
72
 getPatrolled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 handlePageDeletedEvent
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 purgeStories
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 deleteStories
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Extension\Wikistories;
4
5use MediaWiki\Config\Config;
6use MediaWiki\Context\RequestContext;
7use MediaWiki\DomainEvent\DomainEventIngress;
8use MediaWiki\Extension\Wikistories\Hooks\RecentChangesPropagationHooks;
9use MediaWiki\MainConfigNames;
10use MediaWiki\Page\DeletePageFactory;
11use MediaWiki\Page\Event\PageDeletedEvent;
12use MediaWiki\Page\Event\PageRevisionUpdatedEvent;
13use MediaWiki\Page\ProperPageIdentity;
14use MediaWiki\Page\WikiPageFactory;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\RecentChanges\RecentChange;
17use MediaWiki\Revision\RevisionRecord;
18use MediaWiki\Title\Title;
19
20/**
21 * Event subscriber acting as an ingress for relevant events emitted
22 * by MediaWiki core.
23 */
24class StoriesEventIngress extends DomainEventIngress {
25
26    private StoriesCache $storiesCache;
27    private PageLinksSearch $linksSearch;
28    private WikiPageFactory $wikiPageFactory;
29    private DeletePageFactory $deletePageFactory;
30    private bool $useRCPatrol;
31
32    public function __construct(
33        StoriesCache $storiesCache,
34        PageLinksSearch $linksSearch,
35        WikiPageFactory $wikiPageFactory,
36        DeletePageFactory $deletePageFactory,
37        Config $config
38    ) {
39        $this->deletePageFactory = $deletePageFactory;
40        $this->linksSearch = $linksSearch;
41        $this->storiesCache = $storiesCache;
42        $this->wikiPageFactory = $wikiPageFactory;
43
44        $this->useRCPatrol = $config->get( MainConfigNames::UseRCPatrol );
45    }
46
47    /**
48     * When editing a story with the form, it is possible to change the 'Related article'
49     * to change which article the story will be shown on. The link to the new article will
50     * be done automatically with the page links but it will still show on the previous
51     * article because of the long-live stories cache.
52     *
53     * This listener invalidates the stories cache for the old article and
54     * purges stories when article is undeleted.
55     *
56     * Also, when a story is saved (created or edited), it creates a recent changes
57     * entry for the related article so that watchers of that article can
58     * be aware of the story change.
59     *
60     * @noinspection PhpUnused
61     */
62    public function handlePageRevisionUpdatedEvent( PageRevisionUpdatedEvent $event ) {
63        $page = $event->getPage();
64        $revisionRecord = $event->getLatestRevisionAfter();
65
66        // Undeletion in the main namespace
67        if ( $page->getNamespace() === NS_MAIN &&
68            $event->isCreation() &&
69            $event->hasCause( PageRevisionUpdatedEvent::CAUSE_UNDELETE )
70        ) {
71            $this->purgeStories( $page );
72        }
73
74        // Story created or edited
75        if ( $page->getNamespace() !== NS_STORY ||
76            $revisionRecord->getMainContentModel() !== 'story'
77        ) {
78            return;
79        }
80
81        $story = $revisionRecord->getMainContentRaw();
82
83        if ( !( $story instanceof StoryContent ) ) {
84            // not the story content format
85            return;
86        }
87
88        // Invalidate caches
89        $articleTitle = $story->getArticleTitle();
90
91        if ( $articleTitle ) {
92            $this->storiesCache->invalidateForArticle( $articleTitle->getId() );
93        }
94
95        $this->storiesCache->invalidateStory( $page->getId() );
96
97        // Inject RecentChanged entry
98        $article = Title::newFromText( $story->getFromArticle() );
99        $context = RequestContext::getMain();
100        $requestIP = $context->getRequest()->getIP();
101        $patrolled = $this->getPatrolled( $article, $context->getAuthority() );
102
103        $rc = RecentChangesPropagationHooks::makeRecentChangesEntry(
104            $article,
105            $revisionRecord,
106            $event->getPerformer(),
107            $revisionRecord->getComment( RevisionRecord::RAW )->text,
108            $requestIP,
109            $revisionRecord->isMinor(),
110            $event->isBotUpdate(),
111            $patrolled,
112            $event->getEditResult()
113        );
114
115        $rc->save();
116    }
117
118    /**
119     * @param Title $title
120     * @param Authority $performer
121     * @return int
122     */
123    private function getPatrolled( Title $title, Authority $performer ): int {
124        return $this->useRCPatrol && $performer->definitelyCan( 'autopatrol', $title ) ?
125            RecentChange::PRC_AUTOPATROLLED :
126            RecentChange::PRC_UNPATROLLED;
127    }
128
129    /**
130     * Do purge stories when article is deleted.
131     * Invalidate stories cache for the related article.
132     *
133     * @noinspection PhpUnused
134     */
135    public function handlePageDeletedEvent( PageDeletedEvent $event ) {
136        $page = $event->getDeletedPage();
137        $deletedRev = $event->getLatestRevisionBefore();
138
139        // NS_MAIN deletion
140        if ( $page->getNamespace() === NS_MAIN ) {
141            $request = RequestContext::getMain()->getRequest();
142            $authority = RequestContext::getMain()->getAuthority();
143
144            $deleteStories = $request->getBool( 'wpDeleteStory' );
145            if ( $deleteStories ) {
146                self::deleteStories(
147                    $request->getText( 'wpReason' ),
148                    $page,
149                    $authority,
150                    $request->getBool( 'wpSuppress' )
151                );
152            } else {
153                self::purgeStories( $page );
154            }
155
156            return;
157        }
158
159        // NS_STORY deletion
160        if ( $page->getNamespace() !== NS_STORY ) {
161            return;
162        }
163
164        $story = $deletedRev->getMainContentRaw();
165        if ( !( $story instanceof StoryContent ) ) {
166            return;
167        }
168
169        $articleTitle = $story->getArticleTitle();
170        if ( $articleTitle === null ) {
171            return;
172        }
173
174        $articlePageId = $articleTitle->getId();
175        if ( $articlePageId === 0 ) {
176            return;
177        }
178
179        $this->storiesCache->invalidateForArticle( $articlePageId );
180    }
181
182    /**
183     * @param ProperPageIdentity $page
184     */
185    private function purgeStories( ProperPageIdentity $page ) {
186        $storiesId = $this->linksSearch->getPageLinks( $page->getDBkey(), 99 );
187        foreach ( $storiesId as $storyId ) {
188            $page = $this->wikiPageFactory->newFromID( $storyId );
189            $page->doPurge();
190        }
191    }
192
193    private function deleteStories( string $reason, ProperPageIdentity $page, Authority $authority, bool $suppress ) {
194        $storiesId = $this->linksSearch->getPageLinks( $page->getDBkey(), 99 );
195        foreach ( $storiesId as $storyId ) {
196            $page = $this->wikiPageFactory->newFromID( $storyId );
197            $deletePage = $this->deletePageFactory->newDeletePage(
198                $page,
199                $authority
200            );
201            $deletePage
202                ->setSuppress( $suppress )
203                ->deleteIfAllowed( $reason );
204        }
205    }
206}