Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 80 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
SpecialOrphanedTimedText | |
0.00% |
0 / 80 |
|
0.00% |
0 / 15 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
sortDescending | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isExpensive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
isCacheable | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isListed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canExecuteQuery | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
canExecute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
2 | |||
getOrderFields | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
existenceCheck | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
getCorrespondingFile | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preprocessResults | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
formatResult | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * Implements Special:OrphanedTimedText |
4 | * |
5 | * @author Brian Wolff |
6 | * @file |
7 | * @ingroup SpecialPage |
8 | */ |
9 | |
10 | namespace MediaWiki\TimedMediaHandler; |
11 | |
12 | use HtmlArmor; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\Languages\LanguageConverterFactory; |
15 | use MediaWiki\Linker\Linker; |
16 | use MediaWiki\SpecialPage\PageQueryPage; |
17 | use MediaWiki\Title\Title; |
18 | use RepoGroup; |
19 | use Skin; |
20 | use stdClass; |
21 | use Wikimedia\Rdbms\IConnectionProvider; |
22 | use Wikimedia\Rdbms\IDatabase; |
23 | use Wikimedia\Rdbms\IResultWrapper; |
24 | |
25 | /** |
26 | * Lists TimedText pages that don't have a corresponding video. |
27 | * |
28 | * @ingroup SpecialPage |
29 | */ |
30 | class 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 | } |