Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialOrphanedTimedText
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 15
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 sortDescending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 isCacheable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isListed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canExecuteQuery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 canExecute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 existenceCheck
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getCorrespondingFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 formatResult
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Implements Special:OrphanedTimedText
4 *
5 * @author Brian Wolff
6 * @file
7 * @ingroup SpecialPage
8 */
9
10namespace MediaWiki\TimedMediaHandler;
11
12use HtmlArmor;
13use MediaWiki\Html\Html;
14use MediaWiki\Languages\LanguageConverterFactory;
15use MediaWiki\Linker\Linker;
16use MediaWiki\SpecialPage\PageQueryPage;
17use MediaWiki\Title\Title;
18use RepoGroup;
19use Skin;
20use stdClass;
21use Wikimedia\Rdbms\IConnectionProvider;
22use Wikimedia\Rdbms\IDatabase;
23use Wikimedia\Rdbms\IResultWrapper;
24
25/**
26 * Lists TimedText pages that don't have a corresponding video.
27 *
28 * @ingroup SpecialPage
29 */
30class SpecialOrphanedTimedText extends PageQueryPage {
31
32    /** @var array with keys being names of valid files */
33    private $existingFiles;
34
35    /** @var IConnectionProvider */
36    private $dbProvider;
37
38    /** @var LanguageConverterFactory */
39    private $languageConverterFactory;
40
41    /** @var RepoGroup */
42    private $repoGroup;
43
44    /**
45     * @param IConnectionProvider $dbProvider
46     * @param LanguageConverterFactory $languageConverterFactory
47     * @param RepoGroup $repoGroup
48     */
49    public function __construct(
50        IConnectionProvider $dbProvider,
51        LanguageConverterFactory $languageConverterFactory,
52        RepoGroup $repoGroup
53    ) {
54        parent::__construct( 'OrphanedTimedText' );
55        $this->dbProvider = $dbProvider;
56        $this->languageConverterFactory = $languageConverterFactory;
57        $this->repoGroup = $repoGroup;
58    }
59
60    /**
61     * This is alphabetical, so sort ascending.
62     * @return bool
63     */
64    public function sortDescending() {
65        return false;
66    }
67
68    /**
69     * Should this be cached?
70     *
71     * This query is actually almost cheap given the current
72     * number of things in TimedText namespace.
73     * @return bool
74     */
75    public function isExpensive() {
76        return $this->canExecute();
77    }
78
79    /**
80     * Main execution function
81     *
82     * @param string $par subpage
83     */
84    public function execute( $par ) {
85        $this->addHelpLink( 'https://commons.wikimedia.org/wiki/Commons:Timed_Text', true );
86
87        if ( !$this->canExecuteQuery() ) {
88            $this->setHeaders();
89            $this->outputHeader();
90            $this->getOutput()->addWikiMsg( 'orphanedtimedtext-unsupported' );
91            return;
92        }
93        parent::execute( $par );
94    }
95
96    /**
97     * Can we cache the results of this query?
98     *
99     * Only if we support the query.
100     * @return bool
101     */
102    public function isCacheable() {
103        return $this->canExecute();
104    }
105
106    /**
107     * List in Special:SpecialPages?
108     *
109     * @return bool
110     */
111    public function isListed() {
112        return $this->canExecute();
113    }
114
115    /**
116     * Can we execute this special page?
117     *
118     * The query uses a mysql specific feature (substring_index), so disable on non mysql dbs.
119     *
120     * @return bool
121     */
122    private function canExecuteQuery() {
123        $dbr = $this->dbProvider->getReplicaDatabase();
124        return $dbr->getType() === 'mysql';
125    }
126
127    /**
128     * Can we execute this special page
129     *
130     * That is, db is mysql, and TimedText namespace enabled.
131     * @return bool
132     */
133    private function canExecute(): bool {
134        return $this->canExecuteQuery();
135    }
136
137    /**
138     * Get query info
139     *
140     * The query here is meant to retrieve all pages in the TimedText namespace,
141     * such that if you strip the last two extensions (e.g. Foo.bar.baz.en.srt -> Foo.bar.baz)
142     * there is no corresponding img_name in image table. So if there is a page in TimedText
143     * namespace named TimedText:My.Dog.webm.ceb.srt, it will include it in the list provided
144     * that File:My.Dog.webm is not uploaded.
145     *
146     * TimedText does not support file redirects or foreign files, so we don't have
147     * to worry about those.
148     *
149     * Potentially this should maybe also include pages not ending in
150     * .<valid lang code>.srt . However, determining what a valid lang code
151     * is, is pretty hard (although perhaps it could check if its [a-z]{2,3}
152     * however then we've got things like roa-tara, cbk-zam, etc)
153     * and TimedText throws away the final .srt extension and will work with
154     * any extension, so things not ending in .srt arguably aren't oprhaned.
155     *
156     * @note This uses "substring_index" which is a mysql extension.
157     * @return array Standard query info values.
158     */
159    public function getQueryInfo() {
160        $tables = [ 'page', 'image' ];
161        $fields = [
162            'namespace' => 'page_namespace',
163            'title' => 'page_title',
164            'value' => 0,
165        ];
166        $conds = [
167            'img_name' => null,
168            'page_namespace' => $this->getConfig()->get( 'TimedTextNS' ),
169            'page_is_redirect' => 0,
170        ];
171
172        // Now for the complicated bit
173        // Note: This bit is mysql specific. Probably could do something
174        // equivalent in postgres via split_part or regex substr,
175        // but my sql-fu is not good enough to figure out how to do
176        // this in standard sql, or in sqlite.
177        $baseCond = 'substr( page_title, 1, length( page_title ) - '
178            . "length( substring_index( page_title, '.' ,-2 ) ) - 1 )";
179        $joinConds = [
180            'image' => [
181                'LEFT OUTER JOIN',
182                $baseCond . ' = img_name'
183            ]
184        ];
185        return [
186            'tables' => $tables,
187            'fields' => $fields,
188            'conds' => $conds,
189            'join_conds' => $joinConds
190        ];
191    }
192
193    /** @inheritDoc */
194    public function getOrderFields() {
195        return [ 'namespace', 'title' ];
196    }
197
198    /**
199     * Is the TimedText page really orphaned?
200     *
201     * Given a title like "TimedText:Some bit here.webm.en.srt"
202     * check to see if "File:Some bit here.webm" really exists (locally).
203     * @param Title $title
204     * @return bool True if we should cross out the line.
205     */
206    protected function existenceCheck( Title $title ) {
207        $fileTitle = $this->getCorrespondingFile( $title );
208        if ( !$fileTitle ) {
209            return !$title->isKnown();
210        }
211        return !$title->isKnown() ||
212            ( isset( $this->existingFiles[ $fileTitle->getDBKey() ] )
213            && $this->existingFiles[$fileTitle->getDBKey()]->getHandler()
214            && $this->existingFiles[$fileTitle->getDBKey()]->getHandler() instanceof TimedMediaHandler );
215    }
216
217    /**
218     * Given a TimedText title, get the File title
219     *
220     * @param Title $timedText
221     * @return Title|null Title in File namespace. null on error.
222     */
223    private function getCorrespondingFile( Title $timedText ) {
224        $titleParts = explode( '.', $timedText->getDBkey() );
225        $baseParts = array_slice( $titleParts, 0, -2 );
226        return Title::makeTitleSafe( NS_FILE, implode( '.', $baseParts ) );
227    }
228
229    /**
230     * What group to include this page in on Special:SpecialPages
231     * @return string
232     */
233    protected function getGroupName() {
234        return 'media';
235    }
236
237    /**
238     * Preprocess result to do existence checks all at once.
239     *
240     * @param IDatabase $db
241     * @param IResultWrapper $res
242     */
243    public function preprocessResults( $db, $res ) {
244        parent::preprocessResults( $db, $res );
245
246        if ( !$res->numRows() ) {
247            return;
248        }
249
250        $filesToLookFor = [];
251        foreach ( $res as $row ) {
252            $title = Title::makeTitle( $row->namespace, $row->title );
253            $fileTitle = $this->getCorrespondingFile( $title );
254            if ( !$fileTitle ) {
255                continue;
256            }
257            $filesToLookFor[] = [ 'title' => $fileTitle, 'ignoreRedirect' => true ];
258        }
259        $this->existingFiles = $this->repoGroup->getLocalRepo()
260            ->findFiles( $filesToLookFor );
261        $res->seek( 0 );
262    }
263
264    /**
265     * Format the result as a simple link to the page
266     *
267     * Based on parent class but with an existence check added.
268     *
269     * @param Skin $skin
270     * @param stdClass $row Result row
271     * @return string
272     */
273    public function formatResult( $skin, $row ) {
274        $title = Title::makeTitleSafe( $row->namespace, $row->title );
275
276        if ( $title instanceof Title ) {
277            $contLangConv = $this->languageConverterFactory->getLanguageConverter();
278            $text = $contLangConv->convert(
279                htmlspecialchars( $title->getPrefixedText() )
280            );
281            $link = $this->getLinkRenderer()->makeLink( $title, new HtmlArmor( $text ) );
282            if ( $this->existenceCheck( $title ) ) {
283                // File got uploaded since this page was cached
284                $link = '<del>' . $link . '</del>';
285            }
286            return $link;
287        }
288
289        return Html::element( 'span', [ 'class' => 'mw-invalidtitle' ],
290            Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) );
291    }
292}