setHeaders(); $this->getOutput()->addWikiMsg( 'orphanedtimedtext-notimedtext' ); return; } elseif ( !$this->canExecuteQuery() ) { $this->setHeaders(); $this->outputHeader(); $this->getOutput()->addWikiMsg( 'orphanedtimedtext-unsupported' ); return; } return parent::execute( $par ); } /** * Can we cache the results of this query? * * Only if we support the query. * @return bool */ public function isCacheable() { return $this->canExecute(); } /** * List in Special:SpecialPages? * * @return bool */ public function isListed() { return $this->canExecute(); } /** * Can we execute this special page? * * The query uses a mysql specific feature (substring_index), so disable on non mysql dbs. * * @return bool */ private function canExecuteQuery() { $dbr = wfGetDB( DB_SLAVE ); return $dbr->getType() === 'mysql'; } /** * Can we execute this special page * * That is, db is mysql, and TimedText namespace enabled. */ private function canExecute() { global $wgEnableLocalTimedText; return $this->canExecuteQuery() && $wgEnableLocalTimedText; } /** * Get query info * * The query here is meant to retrieve all pages in the TimedText namespace, * such that if you strip the last two extensions (e.g. Foo.bar.baz.en.srt -> Foo.bar.baz) * there is no corresponding img_name in image table. So if there is a page in TimedText * namespace named TimedText:My.Dog.webm.ceb.srt, it will include it in the list provided * that File:My.Dog.webm is not uploaded. * * TimedText does not support file redirects or foreign files, so we don't have * to worry about those. * * Potentially this should maybe also include pages not ending in * ..srt . However, determining what a valid lang code * is, is pretty hard (although perhaps it could check if its [a-z]{2,3} * however then we've got things like roa-tara, cbk-zam, etc) * and TimedText throws away the final .srt extension and will work with * any extension, so things not ending in .srt arguably aren't oprhaned. * * @note This uses "substring_index" which is a mysql extension. * @return Array Standard query info values. */ function getQueryInfo() { $tables = array( 'page', 'image' ); $fields = array( 'namespace' => 'page_namespace', 'title' => 'page_title', 'value' => 0, ); $conds = array( 'img_name' => null, 'page_namespace' => NS_TIMEDTEXT, ); // Now for the complicated bit // Note: This bit is mysql specific. Probably could do something // equivalent in postgress via split_part or regex substr, // but my sql-fu is not good enough to figure out how to do // this in standard sql, or in sqlite. $baseCond = 'substr( page_title, 1, length( page_title ) - ' . "length( substring_index( page_title, '.' ,-2 ) ) - 1 )"; $joinConds = array( 'image' => array( 'LEFT OUTER JOIN', $baseCond . ' = img_name' ) ); return array( 'tables' => $tables, 'fields' => $fields, 'conds' => $conds, 'join_conds' => $joinConds ); } public function getOrderFields() { return array( 'namespace', 'title' ); } /** * Is the TimedText page really orphaned? * * Given a title like "TimedText:Some bit here.webm.en.srt" * check to see if "File:Some bit here.webm" really exists (locally). * @return bool True if we should cross out the line. */ protected function existenceCheck( Title $title ) { $fileTitle = $this->getCorrespondingFile( $title ); if ( !$fileTitle ) { return $title && !$title->isKnown(); } return !$title->isKnown() || ( isset( $this->existingFiles[ $fileTitle->getDBKey() ] ) && $this->existingFiles[$fileTitle->getDBKey()]->getHandler() && $this->existingFiles[$fileTitle->getDBKey()]->getHandler() instanceof TimedMediaHandler ); } /** * Given a TimedText title, get the File title * * @return Title|null Title in File namespace. null on error. */ private function getCorrespondingFile( Title $timedText ) { if ( !$timedText ) { return false; } $titleParts = explode( '.', $timedText->getDBkey() ); array_pop( $titleParts ); array_pop( $titleParts ); $fileTitle = Title::makeTitleSafe( NS_FILE, implode( '.', $titleParts ) ); return $fileTitle; } /** * What group to include this page in on Special:SpecialPages * @return String */ protected function getGroupName() { return 'media'; } /** * Preprocess result to do existence checks all at once. * * @param $db Database * @param $res ResultWraper */ public function preprocessResults( $db, $res ) { parent::preprocessResults( $db, $res ); if ( !$res->numRows() ) { return; } $filesToLookFor = array(); foreach( $res as $row ) { $title = Title::makeTitle( $row->namespace, $row->title ); $fileTitle = $this->getCorrespondingFile( $title ); if ( !$fileTitle ) { continue; } $filesToLookFor[] = array( 'title' => $fileTitle, 'ignoreRedirect' => true ); } $this->existingFiles = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToLookFor ); $res->seek( 0 ); } /** * Format the result as a simple link to the page * * Based on parent class but with an existence check added. * * @param Skin $skin * @param object $row Result row * @return string */ public function formatResult( $skin, $row ) { global $wgContLang; $title = Title::makeTitleSafe( $row->namespace, $row->title ); if ( $title instanceof Title ) { $text = $wgContLang->convert( $title->getPrefixedText() ); $link = Linker::link( $title, htmlspecialchars( $text ) ); if ( $this->existenceCheck( $title ) ) { // File got uploaded since this page was cached $link = '' . $link . ''; } return $link; } else { return Html::element( 'span', array( 'class' => 'mw-invalidtitle' ), Linker::getInvalidTitleDescription( $this->getContext(), $row->namespace, $row->title ) ); } } }