Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 126 |
|
0.00% |
0 / 21 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
0.00% |
0 / 126 |
|
0.00% |
0 / 21 |
3422 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
onCanonicalNamespaces | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
register | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
onImageOpenShowImageInlineBefore | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onImagePageFileHistoryLine | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onImagePageHooks | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onArticleFromTitle | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onArticleContentOnDiff | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
onSkinTemplateNavigation__Universal | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
isTimedMediaHandlerTitle | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
onImagePageAfterImageLinks | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
onFileUpload | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onTitleMove | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
onPageMoveComplete | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
onFileDeleteComplete | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
onFileUndeleteComplete | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onRevisionFromEditComplete | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
onArticlePurge | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
onParserTestGlobals | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
onBeforePageDisplay | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
132 | |||
onwgQueryPages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | // phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
4 | |
5 | namespace MediaWiki\TimedMediaHandler; |
6 | |
7 | use Article; |
8 | use DifferenceEngine; |
9 | use File; |
10 | use IContextSource; |
11 | use ImageHistoryList; |
12 | use ImagePage; |
13 | use LocalFile; |
14 | use MediaWiki\Config\Config; |
15 | use MediaWiki\Diff\Hook\ArticleContentOnDiffHook; |
16 | use MediaWiki\Hook\CanonicalNamespacesHook; |
17 | use MediaWiki\Hook\FileDeleteCompleteHook; |
18 | use MediaWiki\Hook\FileUndeleteCompleteHook; |
19 | use MediaWiki\Hook\FileUploadHook; |
20 | use MediaWiki\Hook\PageMoveCompleteHook; |
21 | use MediaWiki\Hook\ParserTestGlobalsHook; |
22 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
23 | use MediaWiki\Hook\TitleMoveHook; |
24 | use MediaWiki\Linker\LinkRenderer; |
25 | use MediaWiki\Linker\LinkTarget; |
26 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
27 | use MediaWiki\Output\OutputPage; |
28 | use MediaWiki\Page\Hook\ArticleFromTitleHook; |
29 | use MediaWiki\Page\Hook\ArticlePurgeHook; |
30 | use MediaWiki\Page\Hook\ImageOpenShowImageInlineBeforeHook; |
31 | use MediaWiki\Page\Hook\ImagePageAfterImageLinksHook; |
32 | use MediaWiki\Page\Hook\ImagePageFileHistoryLineHook; |
33 | use MediaWiki\Page\Hook\RevisionFromEditCompleteHook; |
34 | use MediaWiki\Revision\RevisionRecord; |
35 | use MediaWiki\SpecialPage\Hook\WgQueryPagesHook; |
36 | use MediaWiki\SpecialPage\SpecialPageFactory; |
37 | use MediaWiki\Status\Status; |
38 | use MediaWiki\TimedMediaHandler\Handlers\TextHandler\TextHandler; |
39 | use MediaWiki\TimedMediaHandler\WebVideoTranscode\WebVideoTranscode; |
40 | use MediaWiki\Title\Title; |
41 | use MediaWiki\User\User; |
42 | use MediaWiki\User\UserIdentity; |
43 | use RepoGroup; |
44 | use Skin; |
45 | use SkinTemplate; |
46 | use WikiFilePage; |
47 | use WikiPage; |
48 | |
49 | /** |
50 | * Hooks for TimedMediaHandler extension |
51 | * |
52 | * @file |
53 | * @ingroup Extensions |
54 | */ |
55 | class Hooks implements |
56 | ArticleContentOnDiffHook, |
57 | ArticleFromTitleHook, |
58 | ArticlePurgeHook, |
59 | BeforePageDisplayHook, |
60 | CanonicalNamespacesHook, |
61 | FileDeleteCompleteHook, |
62 | FileUndeleteCompleteHook, |
63 | FileUploadHook, |
64 | ImageOpenShowImageInlineBeforeHook, |
65 | ImagePageAfterImageLinksHook, |
66 | ImagePageFileHistoryLineHook, |
67 | PageMoveCompleteHook, |
68 | ParserTestGlobalsHook, |
69 | RevisionFromEditCompleteHook, |
70 | SkinTemplateNavigation__UniversalHook, |
71 | TitleMoveHook, |
72 | WgQueryPagesHook |
73 | { |
74 | |
75 | /** @var Config */ |
76 | private $config; |
77 | |
78 | /** @var LinkRenderer */ |
79 | private $linkRenderer; |
80 | |
81 | /** @var RepoGroup */ |
82 | private $repoGroup; |
83 | |
84 | /** @var SpecialPageFactory */ |
85 | private $specialPageFactory; |
86 | |
87 | /** @var TranscodableChecker */ |
88 | private $transcodableChecker; |
89 | |
90 | /** |
91 | * @param Config $config |
92 | * @param LinkRenderer $linkRenderer |
93 | * @param RepoGroup $repoGroup |
94 | * @param SpecialPageFactory $specialPageFactory |
95 | */ |
96 | public function __construct( |
97 | Config $config, |
98 | LinkRenderer $linkRenderer, |
99 | RepoGroup $repoGroup, |
100 | SpecialPageFactory $specialPageFactory |
101 | ) { |
102 | $this->config = $config; |
103 | $this->linkRenderer = $linkRenderer; |
104 | $this->repoGroup = $repoGroup; |
105 | $this->specialPageFactory = $specialPageFactory; |
106 | $this->transcodableChecker = new TranscodableChecker( |
107 | $config, |
108 | $repoGroup |
109 | ); |
110 | } |
111 | |
112 | /** |
113 | * Register TimedMediaHandler namespace IDs |
114 | * |
115 | * This way if you set a variable like $wgTimedTextNS in LocalSettings.php |
116 | * after you include TimedMediaHandler we can still read the variable values |
117 | * |
118 | * These are configurable due to Commons history: T123823 |
119 | * These need to be before registerhooks due to: T123695 |
120 | * |
121 | * @param array &$list |
122 | */ |
123 | public function onCanonicalNamespaces( &$list ) { |
124 | if ( !defined( 'NS_TIMEDTEXT' ) ) { |
125 | $timedTextNS = $this->config->get( 'TimedTextNS' ); |
126 | define( 'NS_TIMEDTEXT', $timedTextNS ); |
127 | define( 'NS_TIMEDTEXT_TALK', $timedTextNS + 1 ); |
128 | } |
129 | |
130 | $list[NS_TIMEDTEXT] = 'TimedText'; |
131 | $list[NS_TIMEDTEXT_TALK] = 'TimedText_talk'; |
132 | } |
133 | |
134 | /** |
135 | * Register remaining TimedMediaHandler hooks right after initial setup |
136 | * |
137 | * TODO: This function shouldn't need to exist. |
138 | * |
139 | * @return bool |
140 | */ |
141 | public static function register() { |
142 | global $wgJobTypesExcludedFromDefaultQueue, |
143 | $wgExcludeFromThumbnailPurge, |
144 | $wgFileExtensions, $wgTmhEnableMp4Uploads, |
145 | $wgTmhFileExtensions; |
146 | |
147 | $wgFileExtensions = array_merge( $wgFileExtensions, $wgTmhFileExtensions ); |
148 | |
149 | // Remove mp4 if not enabled: |
150 | if ( $wgTmhEnableMp4Uploads === false ) { |
151 | $index = array_search( 'mp4', $wgFileExtensions, true ); |
152 | if ( $index !== false ) { |
153 | array_splice( $wgFileExtensions, $index, 1 ); |
154 | } |
155 | } |
156 | |
157 | // Transcode jobs must be explicitly requested from the job queue: |
158 | $wgJobTypesExcludedFromDefaultQueue[] = 'webVideoTranscode'; |
159 | |
160 | // Exclude transcoded assets from normal thumbnail purging |
161 | // ( a maintenance script could handle transcode asset purging) |
162 | if ( isset( $wgExcludeFromThumbnailPurge ) ) { |
163 | $wgExcludeFromThumbnailPurge = array_merge( $wgExcludeFromThumbnailPurge, $wgTmhFileExtensions ); |
164 | // Also add the .log file ( used in two pass encoding ) |
165 | // ( probably should move in-progress encodes out of web accessible directory ) |
166 | $wgExcludeFromThumbnailPurge[] = 'log'; |
167 | } |
168 | |
169 | // validate enabled transcodeset values |
170 | WebVideoTranscode::validateTranscodeConfiguration(); |
171 | return true; |
172 | } |
173 | |
174 | /** |
175 | * @param ImagePage $imagePage the imagepage that is being rendered |
176 | * @param OutputPage $output the output for this imagepage |
177 | * @return bool |
178 | */ |
179 | public function onImageOpenShowImageInlineBefore( $imagePage, $output ) { |
180 | $file = $imagePage->getDisplayedFile(); |
181 | return self::onImagePageHooks( $file, $output ); |
182 | } |
183 | |
184 | /** |
185 | * @param ImageHistoryList $imageHistoryList that is being rendered |
186 | * @param File $file the (old) file added in this history entry |
187 | * @param string &$line the HTML of the history line |
188 | * @param string &$css the CSS class of the history line |
189 | * @return bool |
190 | */ |
191 | public function onImagePageFileHistoryLine( $imageHistoryList, $file, &$line, &$css ) { |
192 | $out = $imageHistoryList->getContext()->getOutput(); |
193 | return self::onImagePageHooks( $file, $out ); |
194 | } |
195 | |
196 | /** |
197 | * @param File $file the file that is being rendered |
198 | * @param OutputPage $out the output to which this file is being rendered |
199 | * @return bool |
200 | */ |
201 | private static function onImagePageHooks( $file, $out ) { |
202 | $handler = $file->getHandler(); |
203 | if ( $handler instanceof TimedMediaHandler ) { |
204 | $out->addModuleStyles( 'ext.tmh.player.styles' ); |
205 | $out->addModules( 'ext.tmh.player' ); |
206 | } |
207 | return true; |
208 | } |
209 | |
210 | /** |
211 | * @param Title $title |
212 | * @param Article|null &$article |
213 | * @param IContextSource $context |
214 | * @return bool |
215 | */ |
216 | public function onArticleFromTitle( $title, &$article, $context ) { |
217 | if ( $title->getNamespace() === $this->config->get( 'TimedTextNS' ) ) { |
218 | $article = new TimedTextPage( $title ); |
219 | } |
220 | return true; |
221 | } |
222 | |
223 | /** |
224 | * @param DifferenceEngine $diffEngine |
225 | * @param OutputPage $output |
226 | * @return bool |
227 | */ |
228 | public function onArticleContentOnDiff( $diffEngine, $output ) { |
229 | if ( $output->getTitle()->getNamespace() === $this->config->get( 'TimedTextNS' ) ) { |
230 | $article = new TimedTextPage( $output->getTitle(), $diffEngine->getNewId() ); |
231 | $article->renderOutput( $output ); |
232 | return false; |
233 | } |
234 | return true; |
235 | } |
236 | |
237 | /** |
238 | * @param SkinTemplate $sktemplate |
239 | * @param array &$links |
240 | */ |
241 | public function onSkinTemplateNavigation__Universal( $sktemplate, &$links ): void { |
242 | if ( $this->isTimedMediaHandlerTitle( $sktemplate->getTitle() ) ) { |
243 | $ttTitle = Title::makeTitleSafe( NS_TIMEDTEXT, $sktemplate->getTitle()->getDBkey() ); |
244 | if ( !$ttTitle ) { |
245 | return; |
246 | } |
247 | $tab = $sktemplate->tabAction( $ttTitle, 'timedtext', false, '', false ); |
248 | |
249 | // Lookup if we have any corresponding timed text available already |
250 | $file = $this->repoGroup->findFile( $sktemplate->getTitle(), [ 'ignoreRedirect' => true ] ); |
251 | $textHandler = new TextHandler( $file, [ TimedTextPage::VTT_SUBTITLE_FORMAT ] ); |
252 | $ttExists = count( $textHandler->getTracks() ) > 0; |
253 | $tab[ 'exists' ] = $ttExists; |
254 | $tab[ 'class' ] .= !$ttExists ? ' new' : ''; |
255 | $links[ 'namespaces' ][ 'timedtext' ] = $tab; |
256 | return; |
257 | } |
258 | if ( $sktemplate->getTitle()->getNamespace() === $this->config->get( 'TimedTextNS' ) ) { |
259 | $page = new TimedTextPage( $sktemplate->getTitle() ); |
260 | $links['namespaces']['file'] = |
261 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
262 | $sktemplate->tabAction( $page->getCorrespondingFileTitle(), 'file', false, '', true ); |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * @param Title $title |
268 | * @return bool |
269 | */ |
270 | public function isTimedMediaHandlerTitle( $title ) { |
271 | if ( !$title->inNamespace( NS_FILE ) ) { |
272 | return false; |
273 | } |
274 | $file = $this->repoGroup->findFile( $title, [ 'ignoreRedirect' => true ] ); |
275 | // Can't find file |
276 | if ( !$file ) { |
277 | return false; |
278 | } |
279 | $handler = $file->getHandler(); |
280 | if ( !$handler ) { |
281 | return false; |
282 | } |
283 | return $handler instanceof TimedMediaHandler; |
284 | } |
285 | |
286 | /** |
287 | * @param Article $imagePage |
288 | * @param string &$html |
289 | * @return bool |
290 | */ |
291 | public function onImagePageAfterImageLinks( $imagePage, &$html ) { |
292 | // load the file: |
293 | $file = $this->repoGroup->findFile( $imagePage->getTitle(), [ 'ignoreRedirect' => true ] ); |
294 | if ( $this->transcodableChecker->isTranscodableFile( $file ) ) { |
295 | $transcodeStatusTable = new TranscodeStatusTable( |
296 | $imagePage->getContext(), |
297 | $this->linkRenderer |
298 | ); |
299 | $html .= $transcodeStatusTable->getHTML( $file ); |
300 | } |
301 | return true; |
302 | } |
303 | |
304 | /** |
305 | * @param File $file LocalFile object |
306 | * @param bool $reupload |
307 | * @param bool $hasDescription |
308 | * @return bool |
309 | */ |
310 | public function onFileUpload( $file, $reupload, $hasDescription ) { |
311 | // Check that the file is a transcodable asset: |
312 | if ( $this->transcodableChecker->isTranscodableFile( $file ) ) { |
313 | // Remove all the transcode files and db states for this asset |
314 | WebVideoTranscode::removeTranscodes( $file ); |
315 | WebVideoTranscode::startJobQueue( $file ); |
316 | } |
317 | return true; |
318 | } |
319 | |
320 | /** |
321 | * Handle moved titles |
322 | * |
323 | * For now we just remove all the derivatives for the oldTitle. In the future we could |
324 | * look at moving the files, but right now thumbs are not moved, so I don't want to be |
325 | * inconsistent. |
326 | * @param Title $title |
327 | * @param Title $newTitle |
328 | * @param User $user |
329 | * @param string $reason |
330 | * @param Status &$status |
331 | * @return bool |
332 | */ |
333 | public function onTitleMove( Title $title, Title $newTitle, User $user, $reason, Status &$status ) { |
334 | if ( $this->transcodableChecker->isTranscodableTitle( $title ) ) { |
335 | // Remove all the transcode files and db states for this asset |
336 | // Will be re-added after the file has moved |
337 | $file = $this->repoGroup->findFile( $title, [ 'ignoreRedirect' => true ] ); |
338 | WebVideoTranscode::removeTranscodes( $file ); |
339 | } |
340 | return true; |
341 | } |
342 | |
343 | /** |
344 | * Hook to PageMoveComplete. Add transcode jobs for new file name |
345 | * @param LinkTarget $old Old title |
346 | * @param LinkTarget $new New title |
347 | * @param UserIdentity $user User who did the move |
348 | * @param int $pageid Database ID of the page that's been moved |
349 | * @param int $redirid Database ID of the created redirect |
350 | * @param string $reason Reason for the move |
351 | * @param RevisionRecord $revision RevisionRecord created by the move |
352 | * @return bool|void True or no return value to continue or false stop other hook handlers, |
353 | * doesn't abort the move itself |
354 | */ |
355 | public function onPageMoveComplete( $old, $new, $user, $pageid, $redirid, $reason, $revision ) { |
356 | if ( $this->transcodableChecker->isTranscodableTitle( $new ) ) { |
357 | $newFile = $this->repoGroup->findFile( $new, [ 'ignoreRedirect' => true, 'latest' => true ] ); |
358 | WebVideoTranscode::startJobQueue( $newFile ); |
359 | } |
360 | } |
361 | |
362 | /** |
363 | * Hook to FileDeleteComplete. Removes transcodes on delete. |
364 | * @param LocalFile $file |
365 | * @param string|null $oldimage |
366 | * @param WikiFilePage|null $article |
367 | * @param User $user |
368 | * @param string $reason |
369 | * @return bool |
370 | */ |
371 | public function onFileDeleteComplete( $file, $oldimage, $article, $user, $reason ) { |
372 | if ( !$oldimage && $this->transcodableChecker->isTranscodableFile( $file ) ) { |
373 | WebVideoTranscode::removeTranscodes( $file ); |
374 | } |
375 | return true; |
376 | } |
377 | |
378 | /** |
379 | * @inheritDoc |
380 | */ |
381 | public function onFileUndeleteComplete( $title, $fileVersions, $user, $reason ) { |
382 | $file = $this->repoGroup->findFile( $title, [ 'ignoreRedirect' => true, 'latest' => true ] ); |
383 | if ( $file && $this->transcodableChecker->isTranscodableFile( $file ) ) { |
384 | WebVideoTranscode::removeTranscodes( $file ); |
385 | WebVideoTranscode::startJobQueue( $file ); |
386 | } |
387 | return true; |
388 | } |
389 | |
390 | /** |
391 | * If file gets reverted to a previous version, reset transcodes. |
392 | * |
393 | * @param WikiPage $wikiPage |
394 | * @param RevisionRecord $rev |
395 | * @param int $originalRevId |
396 | * @param UserIdentity $user |
397 | * @param string[] &$tags |
398 | * |
399 | * @return bool |
400 | */ |
401 | public function onRevisionFromEditComplete( |
402 | $wikiPage, $rev, $originalRevId, $user, &$tags |
403 | ) { |
404 | // Check if the article is a file and remove transcode files: |
405 | if ( ( $originalRevId !== false ) && $wikiPage->getTitle()->getNamespace() === NS_FILE ) { |
406 | $file = $this->repoGroup->findFile( $wikiPage->getTitle() ); |
407 | if ( $this->transcodableChecker->isTranscodableFile( $file ) ) { |
408 | WebVideoTranscode::removeTranscodes( $file ); |
409 | WebVideoTranscode::startJobQueue( $file ); |
410 | } |
411 | } |
412 | return true; |
413 | } |
414 | |
415 | /** |
416 | * When a user asks for a purge, perhaps through our handy "update transcode status" |
417 | * link, make sure we've got the updated set of transcodes. This'll allow a user or |
418 | * automated process to see their status and reset them. |
419 | * |
420 | * @param WikiPage $wikiPage |
421 | * @return bool |
422 | */ |
423 | public function onArticlePurge( $wikiPage ) { |
424 | if ( $wikiPage->getTitle()->getNamespace() === NS_FILE ) { |
425 | $file = $this->repoGroup->findFile( $wikiPage->getTitle(), [ 'ignoreRedirect' => true ] ); |
426 | if ( $this->transcodableChecker->isTranscodableFile( $file ) ) { |
427 | WebVideoTranscode::cleanupTranscodes( $file ); |
428 | } |
429 | } |
430 | return true; |
431 | } |
432 | |
433 | /** |
434 | * @param array &$globals |
435 | */ |
436 | public function onParserTestGlobals( &$globals ) { |
437 | // reset player serial so that parser tests are not order-dependent |
438 | TimedMediaTransformOutput::resetSerialForTest(); |
439 | |
440 | $globals['wgEnableTranscode'] = false; |
441 | $globals['wgFFmpegLocation'] = '/usr/bin/ffmpeg'; |
442 | } |
443 | |
444 | /** |
445 | * Add JavaScript and CSS for special pages that may include timed media |
446 | * but which will not fire the parser hook. |
447 | * |
448 | * FIXME: There ought to be a better interface for determining whether the |
449 | * page is liable to contain timed media. |
450 | * |
451 | * @param OutputPage $out |
452 | * @param Skin $skin |
453 | */ |
454 | public function onBeforePageDisplay( $out, $skin ): void { |
455 | $title = $out->getTitle(); |
456 | $namespace = $title->getNamespace(); |
457 | $addModules = false; |
458 | |
459 | if ( $namespace === NS_CATEGORY || $namespace === $this->config->get( 'TimedTextNS' ) ) { |
460 | $addModules = true; |
461 | } elseif ( $title->isSpecialPage() ) { |
462 | [ $name, ] = $this->specialPageFactory->resolveAlias( $title->getDBkey() ); |
463 | if ( $name !== null && ( |
464 | $name === 'Search' || |
465 | $name === 'GlobalUsage' || |
466 | $name === 'Upload' || |
467 | stripos( $name, 'file' ) !== false || |
468 | stripos( $name, 'image' ) !== false |
469 | ) |
470 | ) { |
471 | $addModules = true; |
472 | } |
473 | } |
474 | |
475 | if ( $addModules ) { |
476 | $out->addModuleStyles( 'ext.tmh.player.styles' ); |
477 | $out->addModules( 'ext.tmh.player' ); |
478 | } |
479 | } |
480 | |
481 | /** |
482 | * @param array &$qp |
483 | */ |
484 | public function onwgQueryPages( &$qp ) { |
485 | $qp[] = [ SpecialOrphanedTimedText::class, 'OrphanedTimedText' ]; |
486 | } |
487 | } |