Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TimedTextPage
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 8
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 view
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 renderOutput
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
156
 doLinkToRemote
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 doRedirectToPageForm
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 onSubmit
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getVideoHTML
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getTimedTextHTML
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * TimedText page display the current video with subtitles to the right.
4 *
5 * Future features for this page
6 * @todo add srt download links
7 * @todo parse and validate srt files
8 * @todo link-in or include the universal subtitles editor
9 */
10
11namespace MediaWiki\TimedMediaHandler;
12
13use Article;
14use File;
15use HTMLForm;
16use LanguageCode;
17use MediaWiki\Html\Html;
18use MediaWiki\Languages\LanguageNameUtils;
19use MediaWiki\Linker\LinkTarget;
20use MediaWiki\MediaWikiServices;
21use MediaWiki\Output\OutputPage;
22use MediaWiki\Revision\RevisionRecord;
23use MediaWiki\Revision\SlotRecord;
24use MediaWiki\Title\Title;
25use Message;
26use TextContent;
27
28class TimedTextPage extends Article {
29
30    /** @var int The width of the video plane. Must much the CSS */
31    private static $videoWidth = 400;
32
33    // WebVTT
34    public const VTT_SUBTITLE_FORMAT = 'vtt';
35
36    // SubRIP (SRT)
37    public const SRT_SUBTITLE_FORMAT = 'srt';
38
39    /** @var string[] */
40    private static $knownTimedTextExtensions = [
41        self::SRT_SUBTITLE_FORMAT,
42        self::VTT_SUBTITLE_FORMAT,
43    ];
44
45    /**
46     * @var LanguageNameUtils
47     */
48    private $languageNameUtils;
49
50    /** @inheritDoc */
51    public function __construct( Title $title, $oldId = null ) {
52        parent::__construct( $title, $oldId );
53        $services = MediaWikiServices::getInstance();
54        $this->languageNameUtils = $services->getLanguageNameUtils();
55    }
56
57    public function view(): void {
58        $request = $this->getContext()->getRequest();
59        $out = $this->getContext()->getOutput();
60        $diff = $request->getVal( 'diff' );
61        // getOldID has side effects
62        $oldid = $this->getOldID();
63
64        if ( $this->mRedirectUrl || isset( $diff ) || $this->getTitle()->getNamespace() !== NS_TIMEDTEXT ) {
65            parent::view();
66            return;
67        }
68        // Article flag is required for some editors, and other features (T307218).
69        $out->setArticleFlag( true );
70
71        $this->showRedirectedFromHeader();
72        $this->showNamespaceHeader();
73
74        $this->renderOutput( $out );
75    }
76
77    /**
78     * Render TimedText to given output
79     * @param OutputPage $out
80     */
81    public function renderOutput( OutputPage $out ): void {
82        // parse page title:
83        $titleParts = explode( '.', $this->getTitle()->getDBkey() );
84        $timedTextExtension = array_pop( $titleParts );
85        $languageKey = array_pop( $titleParts );
86
87        $oldid = $this->getOldID();
88        // Are we looking at an old revision
89        if ( $oldid && $this->fetchRevisionRecord() ) {
90            $out->setRevisionId( $this->getRevIdFetched() );
91            $this->setOldSubtitle( $oldid );
92
93            if ( !$this->showDeletedRevisionHeader() ) {
94                wfDebug( __METHOD__ . ": cannot view deleted revision\n" );
95                return;
96            }
97        }
98
99        $repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
100        // Check for File name without text extension:
101        // i.e TimedText:myfile.ogg
102        $fileTitle = Title::newFromText( $this->getTitle()->getDBkey(), NS_FILE );
103        $file = $repoGroup->findFile( $fileTitle );
104        // Check for a valid srt page, present redirect form for the full title match:
105        if ( $file && $file->exists() &&
106            !in_array( $timedTextExtension, self::$knownTimedTextExtensions, true )
107        ) {
108            if ( $file->isLocal() ) {
109                $this->doRedirectToPageForm();
110            } else {
111                $this->doLinkToRemote( $file );
112            }
113            return;
114        }
115
116        // Check for File name with text extension ( from remaining parts of title )
117        // i.e TimedText:myfile.ogg.en.srt
118
119        $videoTitle = Title::newFromText( implode( '.', $titleParts ), NS_FILE );
120
121        // Check for remote file
122        $basefile = $repoGroup->findFile( $videoTitle );
123        if ( !$basefile ) {
124            $out->addHTML( wfMessage( 'timedmedia-subtitle-no-video' )->escaped() );
125            return;
126        }
127
128        if ( !$basefile->isLocal() ) {
129            $this->doLinkToRemote( $basefile );
130            return;
131        }
132
133        // Look up the language name:
134        $language = $out->getLanguage()->getCode();
135        $languages = $this->languageNameUtils->getLanguageNames( $language, LanguageNameUtils::ALL );
136        $languageName = $languages[$languageKey] ?? $languageKey;
137
138        // Set title
139        $message = $this->getPage()->exists() ?
140            'timedmedia-timedtext-title-edit-subtitles' :
141            'timedmedia-timedtext-title-create-subtitles';
142        $out->setPageTitleMsg( wfMessage( $message, $languageName, $videoTitle ) );
143
144        $out->addHtml(
145            Html::rawElement( 'div', [ 'class' => 'mw-timedtextpage-layout' ],
146                Html::rawElement( 'div', [ 'class' => 'mw-timedtextpage-video' ],
147                    $this->getVideoHTML( $videoTitle )
148                ) .
149                Html::rawElement(
150                    'div',
151                    [ 'class' => 'mw-timedtextpage-tt' ],
152                    $this->getTimedTextHTML( $languageName )
153                )
154            )
155        );
156        $out->addModuleStyles( [ 'ext.tmh.timedtextpage.styles' ] );
157
158        if ( !$oldid ) {
159            // Set wgRevision at the end from what we actually fetched.
160            $out->setRevisionId( $this->getRevIdFetched() );
161        }
162    }
163
164    /**
165     * Timed text or file is hosted on remote repo, Add a short description and link to foring repo
166     * @param File $file the base file
167     */
168    private function doLinkToRemote( File $file ): void {
169        $output = $this->getContext()->getOutput();
170        $output->setPageTitleMsg( wfMessage( 'timedmedia-subtitle-remote',
171            $file->getRepo()->getDisplayName() ) );
172        $output->addHTML( wfMessage( 'timedmedia-subtitle-remote-link',
173            $file->getDescriptionUrl(), $file->getRepo()->getDisplayName() )->parse() );
174    }
175
176    private function doRedirectToPageForm(): void {
177        $context = $this->getContext();
178        $lang = $context->getLanguage();
179        $out = $context->getOutput();
180
181        // Set the page title:
182        $out->setPageTitleMsg( wfMessage( 'timedmedia-subtitle-new' ) );
183
184        $out->enableOOUI();
185
186        $languages = $this->languageNameUtils->getLanguageNames(
187            LanguageNameUtils::AUTONYMS,
188            LanguageNameUtils::SUPPORTED
189        );
190        $options = [];
191        foreach ( $languages as $code => $name ) {
192            $display = LanguageCode::bcp47( $code ) . ' - ' . $name;
193            $options[$display] = $code;
194        }
195
196        $formDescriptor = [
197            'lang' => [
198                'label-message' => 'timedmedia-subtitle-new-desc',
199                'required' => true,
200                'type' => 'select',
201                'options' => $options,
202                'default' => $lang->getCode(),
203            ]
204        ];
205
206        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $context );
207        $htmlForm
208            ->setMethod( 'post' )
209            ->setSubmitTextMsg( 'timedmedia-subtitle-new-go' )
210            ->prepareForm()
211            ->setSubmitCallback( [ $this, 'onSubmit' ] )
212            ->show();
213    }
214
215    /** @inheritDoc */
216    public function onSubmit( array $data ): bool {
217        if ( !empty( $data['lang'] ) ) {
218            $output = $this->getContext()->getOutput();
219            $target = $output->getTitle() . '.' . $data['lang'] . '.' . self::SRT_SUBTITLE_FORMAT;
220            $targetFullUrl = $output->getTitle()->getFullUrl() . '.' . $data['lang'] . '.' . self::SRT_SUBTITLE_FORMAT;
221            if ( Title::newFromText( $target )->exists() ) {
222                $output->redirect( $targetFullUrl );
223            } else {
224                $output->redirect( $targetFullUrl . '?action=edit' );
225            }
226            return true;
227        }
228        return false;
229    }
230
231    /**
232     * Gets the video HTML ( with the current language set as default )
233     * @param LinkTarget $videoTitle
234     * @return string
235     */
236    private function getVideoHTML( LinkTarget $videoTitle ): string {
237        // Get the video embed:
238        $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $videoTitle );
239        if ( !$file ) {
240            return wfMessage( 'timedmedia-subtitle-no-video' )->escaped();
241        }
242
243        $videoTransform = $file->transform(
244            [
245                'width' => self::$videoWidth
246            ]
247        );
248        return $videoTransform->toHtml();
249    }
250
251    /**
252     * Gets an HTML representation of the Timed Text
253     *
254     * @param string $languageName
255     * @return Message|string
256     */
257    private function getTimedTextHTML( string $languageName ) {
258        if ( !$this->getPage()->exists() ) {
259            return wfMessage( 'timedmedia-subtitle-no-subtitles', $languageName );
260        }
261
262        $revision = $this->fetchRevisionRecord();
263        if ( !$revision ) {
264            return wfMessage( 'noarticletext', $languageName );
265        }
266
267        $content = $revision->getContent(
268            SlotRecord::MAIN,
269            RevisionRecord::FOR_THIS_USER,
270            $this->getContext()->getUser()
271        );
272        if ( !$content ) {
273            return wfMessage( 'rev-deleted-text-permission', $languageName );
274        }
275
276        return Html::element(
277            'pre',
278            [],
279            ( $content instanceof TextContent ) ? $content->getText() : null
280        );
281    }
282}