Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 115 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 115 |
|
0.00% |
0 / 16 |
1190 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onGetPreferences | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getDiscoveryMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isBetaDiscoveryMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isPublicDiscoveryMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
purgeStories | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
deleteStories | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getDiscoverBundleData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getArticleSectionTitle | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
onLoginFormValidErrorMessages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onPageSaveComplete | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
onPageDeleteComplete | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
onPageUndeleteComplete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onParserCacheSaveComplete | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
onArticlePurge | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onActionModifyFormFields | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Wikistories; |
4 | |
5 | use Article; |
6 | use ManualLogEntry; |
7 | use MediaWiki\Config\Config; |
8 | use MediaWiki\Context\RequestContext; |
9 | use MediaWiki\Deferred\DeferredUpdates; |
10 | use MediaWiki\Extension\Wikistories\Jobs\ArticleChangedJob; |
11 | use MediaWiki\Hook\ActionModifyFormFieldsHook; |
12 | use MediaWiki\Hook\LoginFormValidErrorMessagesHook; |
13 | use MediaWiki\Hook\ParserCacheSaveCompleteHook; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Page\Hook\ArticlePurgeHook; |
16 | use MediaWiki\Page\Hook\PageDeleteCompleteHook; |
17 | use MediaWiki\Page\Hook\PageUndeleteCompleteHook; |
18 | use MediaWiki\Page\ProperPageIdentity; |
19 | use MediaWiki\Parser\ParserCache; |
20 | use MediaWiki\Parser\ParserOptions; |
21 | use MediaWiki\Parser\ParserOutput; |
22 | use MediaWiki\Permissions\Authority; |
23 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
24 | use MediaWiki\Revision\RevisionRecord; |
25 | use MediaWiki\SpecialPage\SpecialPage; |
26 | use MediaWiki\Storage\EditResult; |
27 | use MediaWiki\Storage\Hook\PageSaveCompleteHook; |
28 | use MediaWiki\Title\Title; |
29 | use MediaWiki\User\User; |
30 | use MediaWiki\User\UserIdentity; |
31 | use WikiPage; |
32 | |
33 | class 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 | } |