Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 132 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
TimedTextPage | |
0.00% |
0 / 132 |
|
0.00% |
0 / 8 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
view | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
renderOutput | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
156 | |||
doLinkToRemote | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
doRedirectToPageForm | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
6 | |||
onSubmit | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getVideoHTML | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getTimedTextHTML | |
0.00% |
0 / 17 |
|
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 | |
11 | namespace MediaWiki\TimedMediaHandler; |
12 | |
13 | use Article; |
14 | use File; |
15 | use HTMLForm; |
16 | use LanguageCode; |
17 | use MediaWiki\Html\Html; |
18 | use MediaWiki\Languages\LanguageNameUtils; |
19 | use MediaWiki\Linker\LinkTarget; |
20 | use MediaWiki\MediaWikiServices; |
21 | use MediaWiki\Output\OutputPage; |
22 | use MediaWiki\Revision\RevisionRecord; |
23 | use MediaWiki\Revision\SlotRecord; |
24 | use MediaWiki\Title\Title; |
25 | use Message; |
26 | use TextContent; |
27 | |
28 | class 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 | } |