Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiTimedText
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 9
756
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
 isInternal
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomPrinter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
156
 findTimedText
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 convertTimedText
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Copyright © 2015 Derk-Jan Hartman "hartman.wiki@gmail.com"
4 * Updated 2017-2019 Brion Vibber <bvibber@wikimedia.org>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @since 1.33
23 */
24
25namespace MediaWiki\TimedMediaHandler;
26
27use ApiBase;
28use ApiFormatRaw;
29use ApiMain;
30use ApiResult;
31use ApiUsageException;
32use File;
33use LogicException;
34use MediaWiki\Languages\LanguageNameUtils;
35use MediaWiki\Page\WikiPageFactory;
36use MediaWiki\TimedMediaHandler\Handlers\TextHandler\TextHandler;
37use MediaWiki\Title\Title;
38use RepoGroup;
39use TextContent;
40use WANObjectCache;
41use Wikimedia\ParamValidator\ParamValidator;
42use WikiPage;
43
44/**
45 * Implements the timedtext module that outputs subtitle files
46 * for consumption by <track> elements
47 *
48 * @ingroup API
49 * @emits error.code timedtext-notfound, invalidlang, invalid-title
50 */
51class ApiTimedText extends ApiBase {
52    /** @var LanguageNameUtils */
53    private $languageNameUtils;
54
55    /** @var RepoGroup */
56    private $repoGroup;
57
58    /** @var WANObjectCache */
59    private $cache;
60
61    /** @var WikiPageFactory */
62    private $wikiPageFactory;
63
64    /** @var int version of the cache format */
65    private const CACHE_VERSION = 1;
66
67    /** @var int default 24 hours */
68    private const CACHE_TTL = 86400;
69
70    /**
71     * @param ApiMain $main
72     * @param string $action
73     * @param LanguageNameUtils $languageNameUtils
74     * @param RepoGroup $repoGroup
75     * @param WANObjectCache $cache
76     * @param WikiPageFactory $wikiPageFactory
77     */
78    public function __construct(
79        ApiMain $main,
80        $action,
81        LanguageNameUtils $languageNameUtils,
82        RepoGroup $repoGroup,
83        WANObjectCache $cache,
84        WikiPageFactory $wikiPageFactory
85    ) {
86        parent::__construct( $main, $action );
87        $this->languageNameUtils = $languageNameUtils;
88        $this->repoGroup = $repoGroup;
89        $this->cache = $cache;
90        $this->wikiPageFactory = $wikiPageFactory;
91    }
92
93    /**
94     * URLs to this API endpoint are intended to be created internally and provided
95     * opaquely in track lists. Not (yet) considered stable for external use.
96     *
97     * @return bool
98     */
99    public function isInternal() {
100        return true;
101    }
102
103    /**
104     * This module uses a raw printer to directly output SRT, VTT or other subtitle formats
105     *
106     * @return ApiFormatRaw
107     */
108    public function getCustomPrinter(): ApiFormatRaw {
109        $printer = new ApiFormatRaw( $this->getMain(), null );
110        $printer->setFailWithHTTPError( true );
111        return $printer;
112    }
113
114    public function execute() {
115        $params = $this->extractRequestParams();
116
117        if ( $params['lang'] === null ) {
118            $langCode = false;
119        } elseif ( !$this->languageNameUtils->isValidCode( $params['lang'] ) ) {
120            $this->dieWithError(
121                [ 'apierror-invalidlang', $this->encodeParamName( 'lang' ) ], 'invalidlang'
122            );
123        } else {
124            $langCode = $params['lang'];
125        }
126
127        $page = $this->getTitleOrPageId( $params );
128        if ( !$page->exists() ) {
129            $this->dieWithError( 'apierror-missingtitle', 'timedtext-notfound' );
130        }
131
132        $ns = $page->getTitle()->getNamespace();
133        if ( $ns !== NS_FILE ) {
134            $this->dieWithError( 'apierror-filedoesnotexist', 'invalidtitle' );
135        }
136        $file = $this->repoGroup->findFile( $page->getTitle() );
137        if ( !$file ) {
138            $this->dieWithError( 'apierror-filedoesnotexist', 'timedtext-notfound' );
139        }
140        if ( !$file->isLocal() ) {
141            $this->dieWithError( 'apierror-timedmedia-notlocal', 'timedtext-notlocal' );
142        }
143
144        // Find subtitle [content] that goes with file
145        $page = $this->findTimedText( $file, $langCode, $params['trackformat'] );
146        if ( !$page ) {
147            $this->dieWithError( 'apierror-timedmedia-lang-notfound', 'timedtext-notfound' );
148        }
149
150        // TODO factor out. dupe with TimedTextPage.php
151        $filename = $page->getTitle()->getDBkey();
152        $titleParts = explode( '.', $filename );
153        $timedTextExtension = array_pop( $titleParts );
154
155        if ( $timedTextExtension !== $params['trackformat'] ) {
156            $filename .= '.' . $params['trackformat'];
157        }
158
159        $rawTimedText = $this->convertTimedText(
160            $timedTextExtension,
161            $params['trackformat'],
162            $page
163        );
164
165        // We want to cache our output
166        $this->getMain()->setCacheMode( 'public' );
167        if ( !$this->getMain()->getParameter( 'smaxage' ) ) {
168            // cache at least 15 seconds.
169            $this->getMain()->setCacheMaxAge( 15 );
170        }
171
172        if ( $params['trackformat'] === TimedTextPage::SRT_SUBTITLE_FORMAT ) {
173            $mimeType = 'text/srt';
174        } elseif ( $params['trackformat'] === TimedTextPage::VTT_SUBTITLE_FORMAT ) {
175            $mimeType = 'text/vtt';
176        } else {
177            // Unreachable due to parameter validation,
178            // unless someone adds a new format and forgets. :D
179            throw new LogicException( 'Unsupported timedtext trackformat' );
180        }
181
182        $result = $this->getResult();
183        $result->addValue( null, 'text', $rawTimedText, ApiResult::NO_SIZE_CHECK );
184        $result->addValue( null, 'mime', $mimeType, ApiResult::NO_SIZE_CHECK );
185        $result->addValue( null, 'filename', $filename, ApiResult::NO_SIZE_CHECK );
186    }
187
188    /**
189     * @param File $file
190     * @param string $langCode
191     * @param string $preferredFormat
192     * @return WikiPage|null
193     * @throws ApiUsageException
194     */
195    protected function findTimedText( File $file, $langCode, $preferredFormat ) {
196        // In future, add TimedTextPage::VTT_SUBTITLE_FORMAT as a supported input format as well.
197        $sourceFormats = [ TimedTextPage::SRT_SUBTITLE_FORMAT ];
198
199        $textHandler = new TextHandler( $file, $sourceFormats );
200        $ns = $textHandler->getTimedTextNamespace();
201        if ( !$ns ) {
202            $this->dieWithError( 'apierror-timedmedia-no-timedtext-support', 'invalidconfig' );
203        }
204
205        foreach ( $sourceFormats as $format ) {
206            $dbkey = "{$file->getTitle()->getDbKey()}.$langCode.$format";
207            $page = $this->wikiPageFactory->newFromTitle( Title::makeTitle( $ns, $dbkey ) );
208            if ( $page->exists() ) {
209                if ( $page->isRedirect() ) {
210                    $title = $page->getRedirectTarget();
211                    if ( $title ) {
212                        $page = $this->wikiPageFactory->newFromTitle( $title );
213                    } else {
214                        return null;
215                    }
216                }
217                if ( $page->exists() ) {
218                    return $page;
219                }
220            }
221        }
222        return null;
223    }
224
225    /**
226     * Fetch and convert or normalize the given timetext source.
227     *
228     * Uses the main WAN cache storage for caching output; if cached
229     * data is available it will be used instead of fetching and
230     * converting the text anew.
231     *
232     * Cache items are auto-expired if the CACHE_VERSION constant
233     * changes or the page has been edited since last update.
234     *
235     * @param string $from one of TimedTextPage::SRT_SUBTITLE_FORMAT or TimedTextPage::VTT_SUBTITLE_FORMAT
236     * @param string $to one of TimedTextPage::VTT_SUBTITLE_FORMAT or TimedTextPage::SRT_SUBTITLE_FORMAT
237     * @param WikiPage $page the TimedText page being loaded
238     * @return string text of the output in desired format
239     */
240    protected function convertTimedText( $from, $to, $page ) {
241        $revId = $page->getLatest();
242        $key = $this->cache->makeKey(
243            'apitimedtext',
244            self::CACHE_VERSION,
245            $page->getTitle()->getDbKey(),
246            $revId,
247            $from,
248            $to
249        );
250        return $this->cache->getWithSetCallback(
251            $key,
252            self::CACHE_TTL,
253            static function ( $cached, &$ttl ) use ( $from, $to, $page ) {
254                // TODO convert to contentmodel
255                $content = $page->getContent();
256                $rawTimedText = $content instanceof TextContent ? $content->getText() : '';
257                return TextHandler::convertSubtitles(
258                    $from,
259                    $to,
260                    $rawTimedText
261                );
262            }
263        );
264    }
265
266    /**
267     * @param int $flags
268     *
269     * @return array
270     */
271    public function getAllowedParams( $flags = 0 ) {
272        $ret = [
273            'title' => [
274                ParamValidator::PARAM_TYPE => 'string',
275            ],
276            'pageid' => [
277                ParamValidator::PARAM_TYPE => 'integer'
278            ],
279            'trackformat' => [
280                ParamValidator::PARAM_TYPE => [
281                    TimedTextPage::SRT_SUBTITLE_FORMAT,
282                    TimedTextPage::VTT_SUBTITLE_FORMAT,
283                ],
284                ParamValidator::PARAM_REQUIRED => true,
285            ],
286            // Note this is the target language of the track to load,
287            // and does not control things like the language of
288            // error messages.
289            //
290            // Should not default to user language or anything, since
291            // track URLs should be consistent and explicit.
292            'lang' => [
293                ParamValidator::PARAM_TYPE => 'string',
294            ],
295        ];
296        return $ret;
297    }
298
299    /**
300     * @see ApiBase::getExamplesMessages()
301     * @return array of examples
302     */
303    protected function getExamplesMessages() {
304        return [
305            'action=timedtext&title=File:Example.ogv&lang=de&trackformat=vtt'
306                => 'apihelp-timedtext-example-1',
307        ];
308    }
309
310    /** @inheritDoc */
311    public function getHelpUrls() {
312        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:TimedMediaHandler';
313    }
314}