diff options
Diffstat (limited to 'www/wiki/extensions/Translate/tag/PageTranslationHooks.php')
-rw-r--r-- | www/wiki/extensions/Translate/tag/PageTranslationHooks.php | 851 |
1 files changed, 608 insertions, 243 deletions
diff --git a/www/wiki/extensions/Translate/tag/PageTranslationHooks.php b/www/wiki/extensions/Translate/tag/PageTranslationHooks.php index c173e449..607440df 100644 --- a/www/wiki/extensions/Translate/tag/PageTranslationHooks.php +++ b/www/wiki/extensions/Translate/tag/PageTranslationHooks.php @@ -4,9 +4,11 @@ * * @file * @author Niklas Laxström - * @license GPL-2.0+ + * @license GPL-2.0-or-later */ +use Wikimedia\ScopedCallback; + /** * Hooks for page translation. * @@ -19,11 +21,17 @@ class PageTranslationHooks { // Check if job queue is running public static $jobQueueRunning = false; + // Check if we are just rendering tags or such + public static $renderingContext = false; + + // Used to communicate data between LanguageLinks and SkinTemplateGetLanguageLink hooks. + private static $languageLinkData = []; + /** * Hook: ParserBeforeStrip - * @param $parser Parser - * @param $text - * @param $state + * @param Parser $parser + * @param string &$text + * @param string $state * @return bool */ public static function renderTagPage( $parser, &$text, $state ) { @@ -33,13 +41,17 @@ class PageTranslationHooks { try { $parse = TranslatablePage::newFromText( $parser->getTitle(), $text )->getParse(); $text = $parse->getTranslationPageText( null ); + $parser->getOutput()->addModuleStyles( 'ext.translate' ); } catch ( TPException $e ) { - // Show ugly preview without processed <translate> tags wfDebug( 'TPException caught; expected' ); } } - $parser->getOutput()->addModules( 'ext.translate' ); + // For section previews, perform additional clean-up, given tags are often + // unbalanced when we preview one section only. + if ( $parser->getOptions()->getIsSectionPreview() ) { + $text = TranslatablePage::cleanupTags( $text ); + } // Set display title $page = TranslatablePage::isTranslationPage( $title ); @@ -47,37 +59,89 @@ class PageTranslationHooks { return true; } + self::$renderingContext = true; list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); $name = $page->getPageDisplayTitle( $code ); - if ( $name ) { $name = $parser->recursivePreprocess( $name ); $parser->getOutput()->setDisplayTitle( $name ); } + self::$renderingContext = false; + + $parser->getOutput()->setExtensionData( + 'translate-translation-page', + [ + 'sourcepagetitle' => $page->getTitle(), + 'languagecode' => $code, + 'messagegroupid' => $page->getMessageGroupId() + ] + ); // Disable edit section links - $parser->getOptions()->setEditSection( false ); + $parser->getOutput()->setExtensionData( 'Translate-noeditsection', true ); + if ( !defined( 'ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) ) { + $parser->getOptions()->setEditSection( false ); + } return true; } /** + * Hook: ParserOutputPostCacheTransform + * @param ParserOutput $out + * @param string &$text + * @param array &$options + */ + public static function onParserOutputPostCacheTransform( + ParserOutput $out, &$text, array &$options + ) { + if ( $out->getExtensionData( 'Translate-noeditsection' ) ) { + $options['enableSectionEditLinks'] = false; + } + } + + /** * Set the right page content language for translated pages ("Page/xx"). * Hook: PageContentLanguage + * + * @param Title $title + * @param Language|StubUserLang|string &$pageLang + * @return true */ - public static function onPageContentLanguage( Title $title, /*string*/&$pageLang ) { + public static function onPageContentLanguage( Title $title, &$pageLang ) { // For translation pages, parse plural, grammar etc with correct language, // and set the right direction if ( TranslatablePage::isTranslationPage( $title ) ) { list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); - $pageLang = $code; + $pageLang = Language::factory( $code ); } return true; } - /// Hook: OutputPageBeforeHTML - public static function injectCss( OutputPage $out, /*string*/$text ) { + /** + * Display an edit notice for translatable source pages if it's enabled + * Hook: TitleGetEditNotices + * + * @param Title $title + * @param int $oldid + * @param array &$notices + */ + public static function onTitleGetEditNotices( Title $title, $oldid, array &$notices ) { + $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage(); + + if ( !$msg->isDisabled() && TranslatablePage::isSourcePage( $title ) ) { + $notices['translate-tag'] = $msg->parseAsBlock(); + } + } + + /** + * Hook: OutputPageBeforeHTML + * @param OutputPage $out + * @param string $text + * @return true + */ + public static function injectCss( OutputPage $out, /*string*/ $text ) { global $wgTranslatePageTranslationULS; $title = $out->getTitle(); @@ -85,8 +149,6 @@ class PageTranslationHooks { $isTranslation = TranslatablePage::isTranslationPage( $title ); if ( $isSource || $isTranslation ) { - $out->addModuleStyles( 'ext.translate' ); - if ( $wgTranslatePageTranslationULS ) { $out->addModules( 'ext.translate.pagetranslation.uls' ); } @@ -106,6 +168,15 @@ class PageTranslationHooks { /** * This is triggered after saves to translation unit pages + * @param WikiPage $wikiPage + * @param User $user + * @param TextContent $content + * @param string $summary + * @param bool $minor + * @param int $flags + * @param Revision $revision + * @param MessageHandle $handle + * @return true */ public static function onSectionSave( WikiPage $wikiPage, User $user, TextContent $content, $summary, $minor, $flags, $revision, MessageHandle $handle @@ -115,12 +186,6 @@ class PageTranslationHooks { return true; } - // Do not trigger renders for fuzzy - $text = $content->getNativeData(); - if ( strpos( $text, TRANSLATE_FUZZY ) !== false ) { - return true; - } - $group = $handle->getGroup(); if ( !$group instanceof WikiPageMessageGroup ) { return true; @@ -165,52 +230,36 @@ class PageTranslationHooks { } /** - * @param $data - * @param $params - * @param $parser Parser + * @param string $data + * @param array $params + * @param Parser $parser * @return string */ public static function languages( $data, $params, $parser ) { - $currentTitle = $parser->getTitle(); + global $wgPageTranslationLanguageList; - // Check if this is a source page or a translation page - $page = TranslatablePage::newFromTitle( $currentTitle ); - if ( $page->getMarkedTag() === false ) { - $page = TranslatablePage::isTranslationPage( $currentTitle ); - } - - if ( $page === false || $page->getMarkedTag() === false ) { + if ( $wgPageTranslationLanguageList === 'sidebar-only' ) { return ''; } - $status = $page->getTranslationPercentages(); - if ( !$status ) { - return ''; - } + self::$renderingContext = true; + $context = new ScopedCallback( function () { + self::$renderingContext = false; + } ); - // If priority languages have been set always show those languages - $priorityLangs = TranslateMetadata::get( $page->getMessageGroupId(), 'prioritylangs' ); - $priorityForce = TranslateMetadata::get( $page->getMessageGroupId(), 'priorityforce' ); - $filter = null; - if ( strlen( $priorityLangs ) > 0 ) { - $filter = array_flip( explode( ',', $priorityLangs ) ); + // Add a dummy language link that is removed in self::addLanguageLinks. + if ( $wgPageTranslationLanguageList === 'sidebar-fallback' ) { + $parser->getOutput()->addLanguageLink( 'x-pagetranslation-tag' ); } - if ( $filter !== null ) { - // If translation is restricted to some languages, only show them - if ( $priorityForce === 'on' ) { - // Do not filter the source language link - $filter[$page->getMessageGroup()->getSourceLanguage()] = true; - $status = array_intersect_key( $status, $filter ); - } - foreach ( $filter as $langCode => $value ) { - if ( !isset( $status[$langCode] ) ) { - // We need to show all priority languages even if no translation started - $status[$langCode] = 0; - } - } + + $currentTitle = $parser->getTitle(); + $pageStatus = self::getTranslatablePageStatus( $currentTitle ); + if ( !$pageStatus ) { + return ''; } - // Fix title + $page = $pageStatus[ 'page' ]; + $status = $pageStatus[ 'languages' ]; $pageTitle = $page->getTitle(); // Sort by language code, which seems to be the only sane method @@ -224,7 +273,7 @@ class PageTranslationHooks { // This should do the same thing for now. $sourceLanguage = $pageTitle->getPageLanguage()->getCode(); - $languages = array(); + $languages = []; foreach ( $status as $code => $percent ) { // Get autonyms (null) $name = TranslateUtils::getLanguageName( $code, null ); @@ -235,7 +284,7 @@ class PageTranslationHooks { $targetTitleString = $pageTitle->getDBkey() . $suffix; $subpage = Title::makeTitle( $pageTitle->getNamespace(), $targetTitleString ); - $classes = array(); + $classes = []; if ( $code === $userLangCode ) { $classes[] = 'mw-pt-languages-ui'; } @@ -243,7 +292,11 @@ class PageTranslationHooks { if ( $currentTitle->equals( $subpage ) ) { $classes[] = 'mw-pt-languages-selected'; $classes = array_merge( $classes, self::tpProgressIcon( $percent ) ); - $name = Html::rawElement( 'span', array( 'class' => $classes ), $name ); + $element = Html::rawElement( + 'span', + [ 'class' => $classes , 'lang' => LanguageCode::bcp47( $code ) ], + $name + ); } elseif ( $subpage->isKnown() ) { $pagename = $page->getPageDisplayTitle( $code ); if ( !is_string( $pagename ) ) { @@ -257,51 +310,56 @@ class PageTranslationHooks { ->params( $pagename ) ->numParams( 100 * $percent ) ->text(); - $attribs = array( + $attribs = [ 'title' => $title, 'class' => $classes, - ); + 'lang' => LanguageCode::bcp47( $code ), + ]; - $name = Linker::linkKnown( $subpage, $name, $attribs ); + $element = Linker::linkKnown( $subpage, $name, $attribs ); } else { /* When language is included because it is a priority language, * but translation does not yet exists, link directly to the * translation view. */ $specialTranslateTitle = SpecialPage::getTitleFor( 'Translate' ); - $params = array( + $params = [ 'group' => $page->getMessageGroupId(), 'language' => $code, 'task' => 'view' - ); + ]; $classes[] = 'new'; // For red link color - $attribs = array( + $attribs = [ 'title' => wfMessage( 'tpt-languages-zero' )->inLanguage( $userLang )->text(), 'class' => $classes, - ); - $name = Linker::link( $specialTranslateTitle, $name, $attribs, $params ); + ]; + $element = Linker::linkKnown( $specialTranslateTitle, $name, $attribs, $params ); } - $languages[] = $name; + $languages[ $name ] = $element; } + // Sort languages by autonym + ksort( $languages ); + $languages = array_values( $languages ); + // dirmark (rlm/lrm) is added, because languages with RTL names can // mess the display $sep = wfMessage( 'tpt-languages-separator' )->inLanguage( $userLang )->escaped(); $sep .= $userLang->getDirMark(); $languages = implode( $sep, $languages ); - $out = Html::openElement( 'div', array( + $out = Html::openElement( 'div', [ 'class' => 'mw-pt-languages noprint', 'lang' => $userLang->getHtmlCode(), 'dir' => $userLang->getDir() - ) ); - $out .= Html::rawElement( 'div', array( 'class' => 'mw-pt-languages-label' ), + ] ); + $out .= Html::rawElement( 'div', [ 'class' => 'mw-pt-languages-label' ], wfMessage( 'tpt-languages-legend' )->inLanguage( $userLang )->escaped() ); $out .= Html::rawElement( 'div', - array( 'class' => 'mw-pt-languages-list autonym' ), + [ 'class' => 'mw-pt-languages-list autonym' ], $languages ); $out .= Html::closeElement( 'div' ); @@ -314,11 +372,11 @@ class PageTranslationHooks { /** * Return icon CSS class for given progress status: percentages * are too accurate and take more space than simple images. - * @param $percent float + * @param float $percent * @return string[] */ protected static function tpProgressIcon( $percent ) { - $classes = array( 'mw-pt-progress' ); + $classes = [ 'mw-pt-progress' ]; $percent *= 100; if ( $percent < 20 ) { $classes[] = 'mw-pt-progress--stub'; @@ -335,8 +393,181 @@ class PageTranslationHooks { } /** + * Returns translatable page and language stats for given title. + * @param Title $title + * @return array|null Returns null if not a translatable page. + */ + private static function getTranslatablePageStatus( Title $title ) { + // Check if this is a source page or a translation page + $page = TranslatablePage::newFromTitle( $title ); + if ( $page->getMarkedTag() === false ) { + $page = TranslatablePage::isTranslationPage( $title ); + } + + if ( $page === false || $page->getMarkedTag() === false ) { + return null; + } + + $status = $page->getTranslationPercentages(); + if ( !$status ) { + return null; + } + + // If priority languages have been set always show those languages + $priorityLangs = TranslateMetadata::get( $page->getMessageGroupId(), 'prioritylangs' ); + $priorityForce = TranslateMetadata::get( $page->getMessageGroupId(), 'priorityforce' ); + $filter = null; + if ( strlen( $priorityLangs ) > 0 ) { + $filter = array_flip( explode( ',', $priorityLangs ) ); + } + if ( $filter !== null ) { + // If translation is restricted to some languages, only show them + if ( $priorityForce === 'on' ) { + // Do not filter the source language link + $filter[$page->getMessageGroup()->getSourceLanguage()] = true; + $status = array_intersect_key( $status, $filter ); + } + foreach ( $filter as $langCode => $value ) { + if ( !isset( $status[$langCode] ) ) { + // We need to show all priority languages even if no translation started + $status[$langCode] = 0; + } + } + } + + return [ + 'page' => $page, + 'languages' => $status + ]; + } + + /** + * Hooks: LanguageLinks + * @param Title $title Title of the page for which links are needed. + * @param array &$languageLinks List of language links to modify. + */ + public static function addLanguageLinks( Title $title, array &$languageLinks ) { + global $wgPageTranslationLanguageList; + + $hasLanguagesTag = false; + foreach ( $languageLinks as $index => $name ) { + if ( $name === 'x-pagetranslation-tag' ) { + $hasLanguagesTag = true; + unset( $languageLinks[ $index ] ); + } + } + + if ( $wgPageTranslationLanguageList === 'tag-only' ) { + return; + } + + if ( $wgPageTranslationLanguageList === 'sidebar-fallback' && $hasLanguagesTag ) { + return; + } + + // $wgPageTranslationLanguageList === 'sidebar-always' OR 'sidebar-only' + + $status = self::getTranslatablePageStatus( $title ); + if ( !$status ) { + return; + } + + self::$renderingContext = true; + $context = new ScopedCallback( function () { + self::$renderingContext = false; + } ); + + $page = $status[ 'page' ]; + $languages = $status[ 'languages' ]; + $en = Language::factory( 'en' ); + + $newLanguageLinks = []; + + // Batch the Title::exists queries used below + $lb = new LinkBatch(); + foreach ( array_keys( $languages ) as $code ) { + $title = $page->getTitle()->getSubpage( $code ); + $lb->addObj( $title ); + } + $lb->execute(); + + foreach ( $languages as $code => $percentage ) { + $title = $page->getTitle()->getSubpage( $code ); + $key = "x-pagetranslation:{$title->getPrefixedText()}"; + $translatedName = $page->getPageDisplayTitle( $code ) ?: $title->getPrefixedText(); + + if ( $title->exists() ) { + $href = $title->getLocalURL(); + $classes = self::tpProgressIcon( $percentage ); + $title = wfMessage( 'tpt-languages-nonzero' ) + ->params( $translatedName ) + ->numParams( 100 * $percentage ); + } else { + $href = SpecialPage::getTitleFor( 'Translate' )->getLocalURL( [ + 'group' => $page->getMessageGroupId(), + 'language' => $code, + ] ); + $classes = [ 'mw-pt-progress--none' ]; + $title = wfMessage( 'tpt-languages-zero' ); + } + + self::$languageLinkData[ $key ] = [ + 'href' => $href, + 'language' => $code, + 'percentage' => $percentage, + 'classes' => $classes, + 'autonym' => $en->ucfirst( Language::fetchLanguageName( $code ) ), + 'title' => $title, + ]; + + $newLanguageLinks[ $key ] = self::$languageLinkData[ $key ][ 'autonym' ]; + } + + asort( $newLanguageLinks ); + $languageLinks = array_merge( array_keys( $newLanguageLinks ), $languageLinks ); + } + + /** + * Hooks: SkinTemplateGetLanguageLink + * @param array &$link + * @param Title $linkTitle + * @param Title $pageTitle + * @param OutputPage $out + */ + public static function formatLanguageLink( + array &$link, + Title $linkTitle, + Title $pageTitle, + OutputPage $out + ) { + if ( substr( $link[ 'text' ], 0, 18 ) !== 'x-pagetranslation:' ) { + return; + } + + if ( !isset( self::$languageLinkData[ $link[ 'text' ] ] ) ) { + return; + } + + $data = self::$languageLinkData[ $link[ 'text' ] ]; + + $link[ 'class' ] .= ' ' . implode( ' ', $data[ 'classes' ] ); + $link[ 'href' ] = $data[ 'href' ]; + $link[ 'text' ] = $data[ 'autonym' ]; + $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text(); + $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] ); + $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] ); + + $out->addModuleStyles( 'ext.translate.tag.languages' ); + } + + /** * Display nice error when editing content. * Hook: EditFilterMergedContent + * @param IContextSource $context + * @param Content $content + * @param Status $status + * @param string $summary + * @return true */ public static function tpSyntaxCheckForEditContent( $context, $content, $status, $summary ) { if ( !$content instanceof TextContent ) { @@ -344,6 +575,8 @@ class PageTranslationHooks { } $text = $content->getNativeData(); + // See T154500 + $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) ); $title = $context->getTitle(); $e = self::tpSyntaxError( $title, $text ); @@ -353,7 +586,7 @@ class PageTranslationHooks { // $msg is an array containing a message key followed by any parameters. // @todo Use Message object instead. - call_user_func_array( array( $status, 'fatal' ), $msg ); + call_user_func_array( [ $status, 'fatal' ], $msg ); } return true; @@ -361,6 +594,9 @@ class PageTranslationHooks { /** * Returns any syntax error. + * @param Title $title + * @param string $text + * @return null|TPException */ protected static function tpSyntaxError( $title, $text ) { if ( strpos( $text, '<translate>' ) === false ) { @@ -381,12 +617,24 @@ class PageTranslationHooks { * When attempting to save, last resort. Edit page would only display * edit conflict if there wasn't tpSyntaxCheckForEditPage. * Hook: PageContentSave + * @param WikiPage $wikiPage + * @param User $user + * @param Content $content + * @param string $summary + * @param bool $minor + * @param string $_1 + * @param bool $_2 + * @param int $flags + * @param Status $status + * @return true */ - public static function tpSyntaxCheck( $wikiPage, $user, $content, $summary, + public static function tpSyntaxCheck( WikiPage $wikiPage, $user, $content, $summary, $minor, $_1, $_2, $flags, $status ) { if ( $content instanceof TextContent ) { $text = $content->getNativeData(); + // See T154500 + $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) ); } else { // Screw it, not interested return true; @@ -401,7 +649,7 @@ class PageTranslationHooks { try { $page->getParse(); } catch ( TPException $e ) { - call_user_func_array( array( $status, 'fatal' ), $e->getMsg() ); + call_user_func_array( [ $status, 'fatal' ], $e->getMsg() ); return false; } @@ -411,8 +659,18 @@ class PageTranslationHooks { /** * Hook: PageContentSaveComplete + * @param WikiPage $wikiPage + * @param User $user + * @param Content $content + * @param string $summary + * @param bool $minor + * @param string $_1 + * @param bool $_2 + * @param int $flags + * @param Revision $revision + * @return true */ - public static function addTranstag( $wikiPage, $user, $content, $summary, + public static function addTranstag( WikiPage $wikiPage, $user, $content, $summary, $minor, $_1, $_2, $flags, $revision ) { // We are not interested in null revisions @@ -454,6 +712,10 @@ class PageTranslationHooks { * at the moment. * Hook: RevisionInsertComplete * @since 2012-05-08 + * @param Revision $rev + * @param string $text + * @param int $flags + * @return true */ public static function updateTranstagOnNullRevisions( Revision $rev, $text, $flags ) { $title = $rev->getTitle(); @@ -467,10 +729,10 @@ class PageTranslationHooks { $dbw = wfGetDB( DB_MASTER ); $table = 'revision'; $field = 'rev_text_id'; - $conds = array( + $conds = [ 'rev_page' => $rev->getPage(), 'rev_id' => $oldRevId, - ); + ]; // FIXME: optimize away this query. Bug T38588. $oldTextId = $dbw->selectField( $table, $field, $conds, __METHOD__ ); @@ -488,18 +750,34 @@ class PageTranslationHooks { } /** - * Prevent editing of unknown pages in Translations namespace. + * Prevent editing of certain pages in Translations namespace. * Hook: getUserPermissionsErrorsExpensive + * + * @param Title $title + * @param User $user + * @param string $action + * @param mixed &$result + * @return bool */ - public static function preventUnknownTranslations( Title $title, User $user, + public static function onGetUserPermissionsErrorsExpensive( Title $title, User $user, $action, &$result ) { $handle = new MessageHandle( $title ); - if ( $action === 'edit' && $handle->isPageTranslation() && - !$handle->isValid() - ) { - $result = array( 'tpt-unknown-page' ); + // Check only when someone tries to edit (or create) page translation messages + if ( $action !== 'edit' || !$handle->isPageTranslation() ) { + return true; + } + + if ( !$handle->isValid() ) { + // Don't allow editing invalid messages that do not belong to any translatable page + $result = [ 'tpt-unknown-page' ]; + return false; + } + + $error = self::getTranslationRestrictions( $handle ); + if ( count( $error ) ) { + $result = $error; return false; } @@ -507,22 +785,17 @@ class PageTranslationHooks { } /** - * Prevent editing of restricted languages. - * Hook: getUserPermissionsErrorsExpensive - * @since 2012-03-01 + * Prevent editing of restricted languages when prioritized. + * + * @param MessageHandle $handle + * @return array array containing error message if restricted, empty otherwise */ - public static function preventRestrictedTranslations( Title $title, User $user, - $action, &$result - ) { + private static function getTranslationRestrictions( MessageHandle $handle ) { global $wgTranslateDocumentationLanguageCode; - // Preventing editing (includes creation) should be enough - if ( $action !== 'edit' ) { - return true; - } - $handle = new MessageHandle( $title ); - if ( !$handle->isValid() ) { - return true; + // Allow adding message documentation even when translation is restricted + if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) { + return []; } // Get the primary group id @@ -532,12 +805,7 @@ class PageTranslationHooks { // Check if anything is prevented for the group in the first place $force = TranslateMetadata::get( $groupId, 'priorityforce' ); if ( $force !== 'on' ) { - return true; - } - - // Allow adding message documentation even when translation is restricted - if ( $handle->getCode() === $wgTranslateDocumentationLanguageCode ) { - return true; + return []; } // And finally check whether the language is not included in whitelist @@ -546,42 +814,45 @@ class PageTranslationHooks { if ( !isset( $filter[$handle->getCode()] ) ) { // @todo Default reason if none provided $reason = TranslateMetadata::get( $groupId, 'priorityreason' ); - $result = array( 'tpt-translation-restricted', $reason ); - - return false; + return [ 'tpt-translation-restricted', $reason ]; } - return true; + return []; } /** * Prevent editing of translation pages directly. * Hook: getUserPermissionsErrorsExpensive + * @param Title $title + * @param User $user + * @param string $action + * @param bool &$result + * @return bool */ public static function preventDirectEditing( Title $title, User $user, $action, &$result ) { - $page = TranslatablePage::isTranslationPage( $title ); - $whitelist = array( - 'read' => true, - 'delete' => true, - 'review' => true, // FlaggedRevs - ); + if ( self::$allowTargetEdit ) { + return true; + } - if ( $page !== false && !isset( $whitelist[$action] ) ) { - if ( self::$allowTargetEdit ) { - return true; - } + $whitelist = [ + 'read', 'delete', 'undelete', 'deletedtext', 'deletedhistory', + 'review', // FlaggedRevs + ]; + if ( in_array( $action, $whitelist ) ) { + return true; + } - if ( $page->getMarkedTag() ) { - list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); - $result = array( - 'tpt-target-page', - ':' . $page->getTitle()->getPrefixedText(), - // This url shouldn't get cached - wfExpandUrl( $page->getTranslationUrl( $code ) ) - ); + $page = TranslatablePage::isTranslationPage( $title ); + if ( $page !== false && $page->getMarkedTag() ) { + list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); + $result = [ + 'tpt-target-page', + ':' . $page->getTitle()->getPrefixedText(), + // This url shouldn't get cached + wfExpandUrl( $page->getTranslationUrl( $code ) ) + ]; - return false; - } + return false; } return true; @@ -594,7 +865,7 @@ class PageTranslationHooks { * @param Title $title * @param User $user * @param string $action - * @param mixed $result + * @param mixed &$result * * @return bool */ @@ -617,15 +888,18 @@ class PageTranslationHooks { * Redirects the delete action to our own for translatable pages. * Hook: ArticleConfirmDelete * - * @param $article Article - * @param $out OutputPage - * @param $reason + * @param Article $article + * @param OutputPage $out + * @param string &$reason * * @return bool */ public static function disableDelete( $article, $out, &$reason ) { $title = $article->getTitle(); - if ( TranslatablePage::isSourcePage( $title ) || + $translatablePage = TranslatablePage::newFromTitle( $title ); + + if ( + $translatablePage->getMarkedTag() !== false || TranslatablePage::isTranslationPage( $title ) ) { $new = SpecialPage::getTitleFor( @@ -641,9 +915,9 @@ class PageTranslationHooks { /** * Hook: ArticleViewHeader * - * @param $article Article - * @param $outputDone - * @param $pcache + * @param Article &$article + * @param bool &$outputDone + * @param bool &$pcache * @return bool */ public static function translatablePageHeader( &$article, &$outputDone, &$pcache ) { @@ -651,58 +925,47 @@ class PageTranslationHooks { return true; } - $title = $article->getTitle(); - - if ( TranslatablePage::isTranslationPage( $title ) ) { - self::translationPageHeader( $title ); + $transPage = TranslatablePage::isTranslationPage( $article->getTitle() ); + $context = $article->getContext(); + if ( $transPage ) { + self::translationPageHeader( $context, $transPage ); } else { // Check for pages that are tagged or marked - self::sourcePageHeader( $title ); + self::sourcePageHeader( $context ); } return true; } - protected static function sourcePageHeader( Title $title ) { - $context = RequestContext::getMain(); + protected static function sourcePageHeader( IContextSource $context ) { + $language = $context->getLanguage(); + $title = $context->getTitle(); $page = TranslatablePage::newFromTitle( $title ); $marked = $page->getMarkedTag(); $ready = $page->getReadyTag(); - - $title = $page->getTitle(); - $latest = $title->getLatestRevID(); - $canmark = $ready === $latest && $marked !== $latest; - - $actions = array(); + $actions = []; if ( $marked && $context->getUser()->isAllowed( 'translate' ) ) { - $par = array( - 'group' => $page->getMessageGroupId(), - 'language' => $context->getLanguage()->getCode(), - 'action' => 'page', - 'filter' => '', - ); - - $translate = SpecialPage::getTitleFor( 'Translate' ); - $linkDesc = $context->msg( 'translate-tag-translate-link-desc' )->escaped(); - $actions[] = Linker::link( $translate, $linkDesc, array(), $par ); + $actions[] = self::getTranslateLink( $context, $page, $language->getCode() ); } - if ( $canmark ) { - $diffUrl = $title->getFullURL( array( 'oldid' => $marked, 'diff' => $latest ) ); - $par = array( 'target' => $title->getPrefixedText(), 'do' => 'mark' ); - $translate = SpecialPage::getTitleFor( 'PageTranslation' ); + $hasChanges = $ready === $latest && $marked !== $latest; + if ( $hasChanges ) { + $diffUrl = $title->getFullURL( [ 'oldid' => $marked, 'diff' => $latest ] ); if ( $context->getUser()->isAllowed( 'pagetranslation' ) ) { - // This page has never been marked + $pageTranslation = SpecialPage::getTitleFor( 'PageTranslation' ); + $params = [ 'target' => $title->getPrefixedText(), 'do' => 'mark' ]; + if ( $marked === false ) { + // This page has never been marked $linkDesc = $context->msg( 'translate-tag-markthis' )->escaped(); - $actions[] = Linker::link( $translate, $linkDesc, array(), $par ); + $actions[] = Linker::linkKnown( $pageTranslation, $linkDesc, [], $params ); } else { - $markUrl = $translate->getFullURL( $par ); + $markUrl = $pageTranslation->getFullURL( $params ); $actions[] = $context->msg( 'translate-tag-markthisagain', $diffUrl, $markUrl ) ->parse(); } @@ -715,28 +978,42 @@ class PageTranslationHooks { return; } - $language = $context->getLanguage(); - $legend = Html::rawElement( + $header = Html::rawElement( 'div', - array( + [ 'class' => 'mw-pt-translate-header noprint nomobile', 'dir' => $language->getDir(), 'lang' => $language->getHtmlCode(), - ), - $context->getLanguage()->semicolonList( $actions ) + ], + $language->semicolonList( $actions ) ) . Html::element( 'hr' ); - $context->getOutput()->addHTML( $legend ); + $context->getOutput()->addHTML( $header ); } - protected static function translationPageHeader( Title $title ) { - if ( !$title->exists() ) { - return; - } + private static function getTranslateLink( + IContextSource $context, TranslatablePage $page, $langCode + ) { + return Linker::linkKnown( + SpecialPage::getTitleFor( 'Translate' ), + $context->msg( 'translate-tag-translate-link-desc' )->escaped(), + [], + [ + 'group' => $page->getMessageGroupId(), + 'language' => $langCode, + 'action' => 'page', + 'filter' => '', + ] + ); + } - // Check if applicable - $page = TranslatablePage::isTranslationPage( $title ); - if ( $page === false ) { + protected static function translationPageHeader( + IContextSource $context, TranslatablePage $page + ) { + global $wgTranslateKeepOutdatedTranslations; + + $title = $context->getTitle(); + if ( !$title->exists() ) { return; } @@ -748,46 +1025,65 @@ class PageTranslationHooks { if ( isset( $pers[$code] ) ) { $per = $pers[$code] * 100; } - $titleText = $page->getTitle()->getPrefixedText(); - // This url might get cached - $url = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE ); - - // Output - $context = RequestContext::getMain(); $language = $context->getLanguage(); - $wrap = Html::rawElement( + $output = $context->getOutput(); + + if ( $page->getSourceLanguageCode() === $code ) { + // If we are on the source language page, link to translate for user's language + $msg = self::getTranslateLink( $context, $page, $language->getCode() ); + } else { + $url = wfExpandUrl( $page->getTranslationUrl( $code ), PROTO_RELATIVE ); + $msg = $context->msg( 'tpt-translation-intro', + $url, + ':' . $page->getTitle()->getPrefixedText(), + $language->formatNum( $per ) + )->parse(); + } + + $header = Html::rawElement( 'div', - array( + [ 'class' => 'mw-pt-translate-header noprint', 'dir' => $language->getDir(), 'lang' => $language->getHtmlCode(), - ), - '$1' - ); + ], + $msg + ) . Html::element( 'hr' ); - $out = $context->getOutput(); + $output->addHTML( $header ); - $out->wrapWikiMsg( - $wrap, - array( - 'tpt-translation-intro', - $url, - ':' . $titleText, - $language->formatNum( $per ) - ) - ); - $out->addHTML( '<hr />' ); + if ( $wgTranslateKeepOutdatedTranslations ) { + $groupId = $page->getMessageGroupId(); + // This is already calculated and cached by above call to getTranslationPercentages + $stats = MessageGroupStats::forItem( $groupId, $code ); + if ( $stats[MessageGroupStats::FUZZY] ) { + // Only show if there is fuzzy messages + $wrap = '<div class="mw-pt-translate-header"><span class="mw-translate-fuzzy">$1</span></div>'; + $output->wrapWikiMsg( $wrap, [ 'tpt-translation-intro-fuzzy' ] ); + } + } } - /// Hook: SpecialPage_initList + /** + * Hook: SpecialPage_initList + * @param array &$list + * @return true + */ public static function replaceMovePage( &$list ) { $list['Movepage'] = 'SpecialPageTranslationMovePage'; return true; } - /// Hook: getUserPermissionsErrorsExpensive + /** + * Hook: getUserPermissionsErrorsExpensive + * @param Title $title + * @param User $user + * @param string $action + * @param array &$result + * @return bool + */ public static function lockedPagesCheck( Title $title, User $user, $action, &$result ) { if ( $action === 'read' ) { return true; @@ -795,9 +1091,8 @@ class PageTranslationHooks { $cache = wfGetCache( CACHE_ANYTHING ); $key = wfMemcKey( 'pt-lock', sha1( $title->getPrefixedText() ) ); - // At least memcached mangles true to "1" - if ( $cache->get( $key ) !== false ) { - $result = array( 'pt-locked-page' ); + if ( $cache->get( $key ) === 'locked' ) { + $result = [ 'pt-locked-page' ]; return false; } @@ -805,9 +1100,16 @@ class PageTranslationHooks { return true; } - /// Hook: SkinSubPageSubtitle - public static function replaceSubtitle( &$subpages, $skin = null, OutputPage $out ) { - if ( !TranslatablePage::isTranslationPage( $out->getTitle() ) + /** + * Hook: SkinSubPageSubtitle + * @param array &$subpages + * @param Skin|null $skin + * @param OutputPage $out + * @return bool + */ + public static function replaceSubtitle( &$subpages, Skin $skin = null, OutputPage $out ) { + $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() ); + if ( !$isTranslationPage && !TranslatablePage::isSourcePage( $out->getTitle() ) ) { return true; @@ -816,23 +1118,24 @@ class PageTranslationHooks { // Copied from Skin::subPageSubtitle() if ( $out->isArticle() && MWNamespace::hasSubpages( $out->getTitle()->getNamespace() ) ) { $ptext = $out->getTitle()->getPrefixedText(); - if ( preg_match( '/\//', $ptext ) ) { + if ( strpos( $ptext, '/' ) !== false ) { $links = explode( '/', $ptext ); array_pop( $links ); - // Also pop of one extra for language code is needed - if ( TranslatablePage::isTranslationPage( $out->getTitle() ) ) { + if ( $isTranslationPage ) { + // Also remove language code page array_pop( $links ); } $c = 0; $growinglink = ''; $display = ''; + $lang = $skin->getLanguage(); foreach ( $links as $link ) { $growinglink .= $link; $display .= $link; $linkObj = Title::newFromText( $growinglink ); - if ( is_object( $linkObj ) && $linkObj->exists() ) { + if ( is_object( $linkObj ) && $linkObj->isKnown() ) { $getlink = Linker::linkKnown( SpecialPage::getTitleFor( 'MyLanguage', $growinglink ), htmlspecialchars( $display ) @@ -841,10 +1144,9 @@ class PageTranslationHooks { $c++; if ( $c > 1 ) { - $subpages .= wfMessage( 'pipe-separator' )->plain(); + $subpages .= $lang->getDirMarkEntity() . $skin->msg( 'pipe-separator' )->escaped(); } else { - // This one is stupid imho, doesn't work with chihuahua - // $subpages .= '< '; + $subpages .= '< '; } $subpages .= $getlink; @@ -863,34 +1165,13 @@ class PageTranslationHooks { return true; } - /// Hook: SpecialTranslate::executeTask - public static function sourceExport( RequestContext $context, - TranslateTask $task = null, MessageGroup $group, array $options - ) { - if ( $task || $options['taction'] !== 'export' - || !$group instanceof WikiPageMessageGroup - ) { - return true; - } - - $page = TranslatablePage::newFromTitle( $group->getTitle() ); - $collection = $group->initCollection( $options['language'] ); - $collection->loadTranslations( DB_MASTER ); - $text = $page->getParse()->getTranslationPageText( $collection ); - $display = $page->getPageDisplayTitle( $options['language'] ); - if ( $display ) { - $text = "{{DISPLAYTITLE:$display}}$text"; - } - $output = Html::element( 'textarea', array( 'rows' => 25 ), $text ); - $context->getOutput()->addHTML( $output ); - - return false; - } - /** * Converts the edit tab (if exists) for translation pages to translate tab. * Hook: SkinTemplateNavigation * @since 2013.06 + * @param Skin $skin + * @param array &$tabs + * @return true */ public static function translateTab( Skin $skin, array &$tabs ) { $title = $skin->getTitle(); @@ -917,17 +1198,26 @@ class PageTranslationHooks { * Hook to update source and destination translation pages on moving translation units * Hook: TitleMoveComplete * @since 2014.08 + * @param Title $ot + * @param Title $nt + * @param User $user + * @param int $oldid + * @param int $newid + * @param string $reason */ public static function onMoveTranslationUnits( Title $ot, Title $nt, User $user, $oldid, $newid, $reason ) { - // Do the update only once. In case running by job queue, the update is not done here - if ( self::$jobQueueRunning ) { + // TranslatablePageMoveJob takes care of handling updates because it performs + // a lot of moves at once. As a performance optimization, skip this hook if + // we detect moves from that job. As there isn't a good way to pass information + // to this hook what originated the move, we use some heuristics. + if ( defined( 'MEDIAWIKI_JOB_RUNNER' ) && $user->equals( FuzzyBot::getUser() ) ) { return; } $groupLast = null; - foreach ( array( $ot, $nt ) as $title ) { + foreach ( [ $ot, $nt ] as $title ) { $handle = new MessageHandle( $title ); if ( !$handle->isValid() ) { continue; @@ -959,4 +1249,79 @@ class PageTranslationHooks { } } } + + /** + * Hook to update translation page on deleting a translation unit + * Hook: ArticleDeleteComplete + * @since 2016.05 + * @param WikiPage &$unit + * @param User &$user + * @param string $reason + * @param int $id + * @param Content $content + * @param ManualLogEntry $logEntry + */ + public static function onDeleteTranslationUnit( WikiPage &$unit, User &$user, $reason, + $id, $content, $logEntry + ) { + // Do the update. In case job queue is doing the work, the update is not done here + if ( self::$jobQueueRunning ) { + return; + } + $title = $unit->getTitle(); + + $handle = new MessageHandle( $title ); + if ( !$handle->isValid() ) { + return; + } + + $group = $handle->getGroup(); + if ( !$group instanceof WikiPageMessageGroup ) { + return; + } + + // There could be interfaces which may allow mass deletion (eg. Nuke). Since they could + // delete many units in one request, it may do several unnecessary edits and cause several + // other unnecessary updates to be done slowing down the user. To avoid that, we push this + // to a queue that is run after the current transaction is committed so that we can see the + // version that is after all the deletions has been done. This allows us to do just one edit + // per translation page after the current deletions has been done. This is sort of hackish + // but this is better user experience and is also more efficent. + static $queuedPages = []; + $target = $group->getTitle(); + $langCode = $handle->getCode(); + $targetPage = $target->getSubpage( $langCode )->getPrefixedText(); + + if ( !isset( $queuedPages[ $targetPage ] ) ) { + $queuedPages[ $targetPage ] = true; + $fname = __METHOD__; + + $dbw = wfGetDB( DB_MASTER ); + $dbw->onTransactionIdle( function () use ( $dbw, $queuedPages, $targetPage, + $target, $handle, $langCode, $user, $reason, $fname + ) { + $dbw->startAtomic( $fname ); + + $page = TranslatablePage::newFromTitle( $target ); + + MessageGroupStats::forItem( + $page->getMessageGroupId(), + $langCode, + MessageGroupStats::FLAG_NO_CACHE + ); + + if ( !$handle->isDoc() ) { + // Assume that $user and $reason for the first deletion is the same for all + self::updateTranslationPage( $page, $langCode, $user, 0, $reason ); + } + + // If a unit was deleted after the edit here is done, this allows us + // to add the page back to the queue again and so we can make another + // edit here with the latest changes. + unset( $queuedPages[ $targetPage ] ); + + $dbw->endAtomic( $fname ); + } ); + } + } } |