diff options
Diffstat (limited to 'MLEB/Translate/tag')
-rw-r--r-- | MLEB/Translate/tag/PageTranslationHooks.php | 456 | ||||
-rw-r--r-- | MLEB/Translate/tag/SpecialPageMigration.php | 12 | ||||
-rw-r--r-- | MLEB/Translate/tag/SpecialPagePreparation.php | 2 | ||||
-rw-r--r-- | MLEB/Translate/tag/SpecialPageTranslation.php | 527 | ||||
-rw-r--r-- | MLEB/Translate/tag/SpecialPageTranslationDeletePage.php | 184 | ||||
-rw-r--r-- | MLEB/Translate/tag/SpecialPageTranslationMovePage.php | 209 | ||||
-rw-r--r-- | MLEB/Translate/tag/TPParse.php | 27 | ||||
-rw-r--r-- | MLEB/Translate/tag/TPSection.php | 31 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslatablePage.php | 197 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslatablePageMoveJob.php | 14 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslateDeleteJob.php | 34 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslateMoveJob.php | 221 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslateRenderJob.php | 26 | ||||
-rw-r--r-- | MLEB/Translate/tag/TranslationsUpdateJob.php | 37 |
14 files changed, 1054 insertions, 923 deletions
diff --git a/MLEB/Translate/tag/PageTranslationHooks.php b/MLEB/Translate/tag/PageTranslationHooks.php index 76d03c8c..205987f5 100644 --- a/MLEB/Translate/tag/PageTranslationHooks.php +++ b/MLEB/Translate/tag/PageTranslationHooks.php @@ -7,6 +7,12 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot; +use MediaWiki\Linker\LinkTarget; +use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionRecord; +use MediaWiki\User\UserIdentity; use Wikimedia\ScopedCallback; /** @@ -28,16 +34,17 @@ class PageTranslationHooks { private static $languageLinkData = []; /** - * Hook: ParserBeforeStrip + * Hook: ParserBeforeInternalParse * @param Parser $parser * @param string &$text + * @param-taint $text escapes_htmlnoent * @param string $state * @return bool */ public static function renderTagPage( $parser, &$text, $state ) { $title = $parser->getTitle(); - if ( strpos( $text, '<translate>' ) !== false ) { + if ( preg_match( '~</?translate[ >]~', $text ) !== 0 ) { try { $parse = TranslatablePage::newFromText( $parser->getTitle(), $text )->getParse(); $text = $parse->getTranslationPageText( null ); @@ -60,10 +67,11 @@ class PageTranslationHooks { } self::$renderingContext = true; - list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); + [ , $code ] = TranslateUtils::figureMessage( $title->getText() ); $name = $page->getPageDisplayTitle( $code ); if ( $name ) { $name = $parser->recursivePreprocess( $name ); + $name = $parser->getTargetLanguage()->convert( $name ); $parser->getOutput()->setDisplayTitle( $name ); } self::$renderingContext = false; @@ -79,9 +87,6 @@ class PageTranslationHooks { // Disable edit section links $parser->getOutput()->setExtensionData( 'Translate-noeditsection', true ); - if ( !defined( 'ParserOutput::SUPPORTS_STATELESS_TRANSFORMS' ) ) { - $parser->getOptions()->setEditSection( false ); - } return true; } @@ -105,14 +110,14 @@ class PageTranslationHooks { * Hook: PageContentLanguage * * @param Title $title - * @param Language &$pageLang + * @param Language|StubUserLang|string &$pageLang * @return true */ 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() ); + [ , $code ] = TranslateUtils::figureMessage( $title->getText() ); $pageLang = Language::factory( $code ); } @@ -127,21 +132,33 @@ class PageTranslationHooks { * @param int $oldid * @param array &$notices */ - public static function onTitleGetEditNotices( Title $title, $oldid, array &$notices ) { - $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage(); + public static function onTitleGetEditNotices( Title $title, int $oldid, array &$notices ) { + if ( TranslatablePage::isSourcePage( $title ) ) { + $msg = wfMessage( 'translate-edit-tag-warning' )->inContentLanguage(); + if ( !$msg->isDisabled() ) { + $notices['translate-tag'] = $msg->parseAsBlock(); + } + + $label = wfMessage( 'tps-edit-sourcepage-title' )->escaped(); + $msg = Html::rawElement( + 'div', + [], + wfMessage( 'tps-edit-sourcepage-text' )->parse() + ); - if ( !$msg->isDisabled() && TranslatablePage::isSourcePage( $title ) ) { - $notices['translate-tag'] = $msg->parseAsBlock(); + $notices[] = TranslateUtils::fieldset( + $label, $msg, [ 'class' => 'mw-infobox translate-edit-documentation' ] + ); } } /** - * Hook: OutputPageBeforeHTML + * Hook: BeforePageDisplay * @param OutputPage $out - * @param string $text + * @param Skin $skin * @return true */ - public static function injectCss( OutputPage $out, /*string*/$text ) { + public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) { global $wgTranslatePageTranslationULS; $title = $out->getTitle(); @@ -153,6 +170,12 @@ class PageTranslationHooks { $out->addModules( 'ext.translate.pagetranslation.uls' ); } + if ( $isSource && TranslateUtils::isEditPage( $out->getContext()->getRequest() ) ) { + // Adding a help notice + $out->addModuleStyles( 'ext.translate.edit.documentation.styles' ); + $out->addModules( 'ext.translate.edit.documentation' ); + } + if ( $isTranslation ) { // Source pages get this module via <translate>, but for translation // pages we need to add it manually. @@ -174,15 +197,14 @@ class PageTranslationHooks { * @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 + $summary, $minor, $flags, MessageHandle $handle ) { // FuzzyBot may do some duplicate work already worked on by other jobs - if ( FuzzyBot::getName() === $user->getName() ) { + if ( $user->equals( FuzzyBot::getUser() ) ) { return true; } @@ -197,7 +219,11 @@ class PageTranslationHooks { // Update the target translation page if ( !$handle->isDoc() ) { $code = $handle->getCode(); - self::updateTranslationPage( $page, $code, $user, $flags, $summary ); + DeferredUpdates::addCallableUpdate( + function () use ( $page, $code, $user, $flags, $summary ) { + self::updateTranslationPage( $page, $code, $user, $flags, $summary ); + } + ); } return true; @@ -294,7 +320,7 @@ class PageTranslationHooks { $classes = array_merge( $classes, self::tpProgressIcon( $percent ) ); $element = Html::rawElement( 'span', - [ 'class' => $classes , 'lang' => TranslateUtils::bcp47( $code ) ], + [ 'class' => $classes , 'lang' => LanguageCode::bcp47( $code ) ], $name ); } elseif ( $subpage->isKnown() ) { @@ -313,7 +339,7 @@ class PageTranslationHooks { $attribs = [ 'title' => $title, 'class' => $classes, - 'lang' => TranslateUtils::bcp47( $code ), + 'lang' => LanguageCode::bcp47( $code ), ]; $element = Linker::linkKnown( $subpage, $name, $attribs ); @@ -554,8 +580,8 @@ class PageTranslationHooks { $link[ 'href' ] = $data[ 'href' ]; $link[ 'text' ] = $data[ 'autonym' ]; $link[ 'title' ] = $data[ 'title' ]->inLanguage( $out->getLanguage()->getCode() )->text(); - $link[ 'lang'] = wfBCP47( $data[ 'language' ] ); - $link[ 'hreflang'] = wfBCP47( $data[ 'language' ] ); + $link[ 'lang'] = LanguageCode::bcp47( $data[ 'language' ] ); + $link[ 'hreflang'] = LanguageCode::bcp47( $data[ 'language' ] ); $out->addModuleStyles( 'ext.translate.tag.languages' ); } @@ -570,16 +596,7 @@ class PageTranslationHooks { * @return true */ public static function tpSyntaxCheckForEditContent( $context, $content, $status, $summary ) { - if ( !$content instanceof TextContent ) { - return true; // whatever. - } - - $text = $content->getNativeData(); - // See T154500 - $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) ); - $title = $context->getTitle(); - - $e = self::tpSyntaxError( $title, $text ); + $e = self::tpSyntaxError( $context->getTitle(), $content ); if ( $e ) { $msg = $e->getMsg(); @@ -592,25 +609,31 @@ class PageTranslationHooks { return true; } - /** - * Returns any syntax error. - * @param Title $title - * @param string $text - * @return null|TPException - */ - protected static function tpSyntaxError( $title, $text ) { - if ( strpos( $text, '<translate>' ) === false ) { + /** Returns any syntax error */ + protected static function tpSyntaxError( ?Title $title, Content $content ): ?TPException { + if ( !$content instanceof TextContent || !$title ) { + return null; + } + + $text = $content->getNativeData(); + + // See T154500 + $text = str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) ); + + if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) { return null; } $page = TranslatablePage::newFromText( $title, $text ); + + $exception = null; try { $page->getParse(); - - return null; } catch ( TPException $e ) { - return $e; + $exception = $e; } + + return $exception; } /** @@ -631,34 +654,64 @@ class PageTranslationHooks { public static function tpSyntaxCheck( WikiPage $wikiPage, $user, $content, $summary, $minor, $_1, $_2, $flags, $status ) { + $e = self::tpSyntaxError( $wikiPage->getTitle(), $content ); + if ( $e ) { + call_user_func_array( [ $status, 'fatal' ], $e->getMsg() ); + + return false; + } + + return true; + } + + /** + * Hook: PageSaveComplete + * + * Only run in versions of mediawiki beginning 1.35; before 1.35, ::addTranstag is used + * + * @param WikiPage $wikiPage + * @param UserIdentity $userIdentity + * @param string $summary + * @param int $flags + * @param RevisionRecord $revisionRecord + * @param mixed $editResult documented as mixed because the EditResult class didn't exist + * before 1.35 + * @return true + */ + public static function addTranstagAfterSave( + WikiPage $wikiPage, + UserIdentity $userIdentity, + string $summary, + int $flags, + RevisionRecord $revisionRecord, + $editResult + ) { + $content = $wikiPage->getContent(); + 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; } // Quick escape on normal pages - if ( strpos( $text, '<translate>' ) === false ) { + if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) { return true; } - $page = TranslatablePage::newFromText( $wikiPage->getTitle(), $text ); - try { - $page->getParse(); - } catch ( TPException $e ) { - call_user_func_array( [ $status, 'fatal' ], $e->getMsg() ); - - return false; - } + // Add the ready tag + $page = TranslatablePage::newFromTitle( $wikiPage->getTitle() ); + $page->addReadyTag( $revisionRecord->getId() ); return true; } /** * Hook: PageContentSaveComplete + * + * Only run in versions of mediawiki before 1.35; in 1.35+, ::addTranstag is used + * * @param WikiPage $wikiPage * @param User $user * @param Content $content @@ -686,7 +739,7 @@ class PageTranslationHooks { } // Quick escape on normal pages - if ( strpos( $text, '</translate>' ) === false ) { + if ( preg_match( '~</?translate[ >]~', $text ) === 0 ) { return true; } @@ -710,47 +763,30 @@ class PageTranslationHooks { * update the translation pages in the case, the non-text changes affect * the rendering of translation pages. I'm not aware of any such cases * at the moment. - * Hook: RevisionInsertComplete + * Hook: RevisionRecordInserted * @since 2012-05-08 - * @param Revision $rev - * @param string $text - * @param int $flags + * @param RevisionRecord $rev * @return true */ - public static function updateTranstagOnNullRevisions( Revision $rev, $text, $flags ) { - $title = $rev->getTitle(); - - $newRevId = $rev->getId(); - $oldRevId = $rev->getParentId(); - $newTextId = $rev->getTextId(); + public static function updateTranstagOnNullRevisions( RevisionRecord $rev ) { + $prevRev = MediaWikiServices::getInstance()->getRevisionLookup() + ->getPreviousRevision( $rev ); - /* This hook doesn't provide any way to detech null revisions - * without extra query */ - $dbw = wfGetDB( DB_MASTER ); - $table = 'revision'; - $field = 'rev_text_id'; - $conds = [ - 'rev_page' => $rev->getPage(), - 'rev_id' => $oldRevId, - ]; - // FIXME: optimize away this query. Bug T38588. - $oldTextId = $dbw->selectField( $table, $field, $conds, __METHOD__ ); - - if ( (string)$newTextId !== (string)$oldTextId ) { + if ( !$prevRev || $prevRev->getSha1() !== $rev->getSha1() ) { // Not a null revision, bail out. return true; } + $title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() ); $page = TranslatablePage::newFromTitle( $title ); - if ( $page->getReadyTag() === $oldRevId ) { - $page->addReadyTag( $newRevId ); + if ( $page->getReadyTag() === $prevRev->getId() ) { + $page->addReadyTag( $rev->getId() ); } - return true; } /** - * Prevent editing of certain pages in Translations namespace. + * Prevent creation of orphan translation units in Translations namespace. * Hook: getUserPermissionsErrorsExpensive * * @param Title $title @@ -759,38 +795,84 @@ class PageTranslationHooks { * @param mixed &$result * @return bool */ - public static function onGetUserPermissionsErrorsExpensive( Title $title, User $user, - $action, &$result + public static function onGetUserPermissionsErrorsExpensive( + Title $title, User $user, $action, &$result ) { $handle = new MessageHandle( $title ); - // Check only when someone tries to edit (or create) page translation messages - if ( $action !== 'edit' || !$handle->isPageTranslation() ) { + // Check only when someone tries to create translation units. + // Allow editing units that become orphaned in regular use, so that + // people can delete them or fix links or other issues in them. + if ( $action !== 'create' || !$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; + $isValid = true; + $groupId = null; + + if ( $handle->isValid() ) { + $groupId = $handle->getGroup()->getId(); + } else { + // Sometimes the message index can be out of date. Either the rebuild job failed or + // it just hasn't finished yet. Do a secondary check to make sure we are not + // inconveniencing translators for no good reason. + // See https://phabricator.wikimedia.org/T221119 + MediaWikiServices::getInstance()->getStatsdDataFactory() + ->increment( 'translate.slow_translatable_page_check' ); + $translatablePage = self::checkTranslatablePageSlow( $title ); + if ( $translatablePage ) { + $groupId = $translatablePage->getMessageGroupId(); + } else { + $isValid = false; + } } - $error = self::getTranslationRestrictions( $handle ); - if ( count( $error ) ) { - $result = $error; - return false; + if ( $isValid ) { + $error = self::getTranslationRestrictions( $handle, $groupId ); + $result = $error ?: $result; + return $error === []; } - return true; + // Don't allow editing invalid messages that do not belong to any translatable page + LoggerFactory::getInstance( 'Translate' )->info( + 'Unknown translation page: {title}', + [ 'title' => $title->getPrefixedDBkey() ] + ); + $result = [ 'tpt-unknown-page' ]; + return false; + } + + private static function checkTranslatablePageSlow( LinkTarget $unit ) : ?TranslatablePage { + $parts = TranslatablePage::parseTranslationUnit( $unit ); + $translationPageTitle = Title::newFromText( + $parts[ 'sourcepage' ] . '/' . $parts[ 'language' ] + ); + if ( !$translationPageTitle ) { + return null; + } + + $translatablePage = TranslatablePage::isTranslationPage( $translationPageTitle ); + if ( !$translatablePage ) { + return null; + } + + $sections = $translatablePage->getSections(); + + if ( !in_array( $parts[ 'section' ], $sections ) ) { + return null; + } + + return $translatablePage; } /** * Prevent editing of restricted languages when prioritized. * * @param MessageHandle $handle + * @param string $groupId * @return array array containing error message if restricted, empty otherwise */ - private static function getTranslationRestrictions( MessageHandle $handle ) { + private static function getTranslationRestrictions( MessageHandle $handle, $groupId ) { global $wgTranslateDocumentationLanguageCode; // Allow adding message documentation even when translation is restricted @@ -798,10 +880,6 @@ class PageTranslationHooks { return []; } - // Get the primary group id - $ids = $handle->getGroupIds(); - $groupId = $ids[0]; - // Check if anything is prevented for the group in the first place $force = TranslateMetadata::get( $groupId, 'priorityforce' ); if ( $force !== 'on' ) { @@ -837,6 +915,7 @@ class PageTranslationHooks { $whitelist = [ 'read', 'delete', 'undelete', 'deletedtext', 'deletedhistory', 'review', // FlaggedRevs + 'patrol', // T151172 ]; if ( in_array( $action, $whitelist ) ) { return true; @@ -844,7 +923,7 @@ class PageTranslationHooks { $page = TranslatablePage::isTranslationPage( $title ); if ( $page !== false && $page->getMarkedTag() ) { - list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); + [ , $code ] = TranslateUtils::figureMessage( $title->getText() ); $result = [ 'tpt-target-page', ':' . $page->getTitle()->getPrefixedText(), @@ -859,32 +938,6 @@ class PageTranslationHooks { } /** - * Prevent patrol links from appearing on translation pages. - * Hook: getUserPermissionsErrors - * - * @param Title $title - * @param User $user - * @param string $action - * @param mixed &$result - * - * @return bool - */ - public static function preventPatrolling( Title $title, User $user, $action, &$result ) { - if ( $action !== 'patrol' ) { - return true; - } - - $page = TranslatablePage::isTranslationPage( $title ); - - if ( $page !== false ) { - $result = [ 'tpt-patrolling-blocked' ]; - return false; - } - - return true; - } - - /** * Redirects the delete action to our own for translatable pages. * Hook: ArticleConfirmDelete * @@ -915,12 +968,12 @@ class PageTranslationHooks { /** * Hook: ArticleViewHeader * - * @param Article &$article + * @param Article $article * @param bool &$outputDone * @param bool &$pcache * @return bool */ - public static function translatablePageHeader( &$article, &$outputDone, &$pcache ) { + public static function translatablePageHeader( $article, &$outputDone, &$pcache ) { if ( $article->getOldID() ) { return true; } @@ -986,7 +1039,7 @@ class PageTranslationHooks { 'lang' => $language->getHtmlCode(), ], $language->semicolonList( $actions ) - ) . Html::element( 'hr' ); + ); $context->getOutput()->addHTML( $header ); } @@ -1017,7 +1070,7 @@ class PageTranslationHooks { return; } - list( , $code ) = TranslateUtils::figureMessage( $title->getText() ); + [ , $code ] = TranslateUtils::figureMessage( $title->getText() ); // Get the translation percentage $pers = $page->getTranslationPercentages(); @@ -1049,7 +1102,7 @@ class PageTranslationHooks { 'lang' => $language->getHtmlCode(), ], $msg - ) . Html::element( 'hr' ); + ); $output->addHTML( $header ); @@ -1103,11 +1156,11 @@ class PageTranslationHooks { /** * Hook: SkinSubPageSubtitle * @param array &$subpages - * @param Skin|null $skin + * @param ?Skin $skin * @param OutputPage $out * @return bool */ - public static function replaceSubtitle( &$subpages, Skin $skin = null, OutputPage $out ) { + public static function replaceSubtitle( &$subpages, ?Skin $skin, OutputPage $out ) { $isTranslationPage = TranslatablePage::isTranslationPage( $out->getTitle() ); if ( !$isTranslationPage && !TranslatablePage::isSourcePage( $out->getTitle() ) @@ -1196,7 +1249,78 @@ class PageTranslationHooks { /** * Hook to update source and destination translation pages on moving translation units + * Hook: PageMoveComplete + * + * Only run in versions of mediawiki beginning 1.35; before 1.35, ::onMoveTranslationUnits is used + * + * @param LinkTarget $oldLinkTarget + * @param LinkTarget $newLinkTarget + * @param UserIdentity $userIdentity + * @param int $oldid + * @param int $newid + * @param string $reason + * @param RevisionRecord $revisionRecord + */ + public static function onMovePageTranslationUnits( + LinkTarget $oldLinkTarget, + LinkTarget $newLinkTarget, + UserIdentity $userIdentity, + int $oldid, + int $newid, + string $reason, + RevisionRecord $revisionRecord + ) { + $user = User::newFromIdentity( $userIdentity ); + // 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; + } + + $oldTitle = Title::newFromLinkTarget( $oldLinkTarget ); + $newTitle = Title::newFromLinkTarget( $newLinkTarget ); + $groupLast = null; + foreach ( [ $oldTitle, $newTitle ] as $title ) { + $handle = new MessageHandle( $title ); + if ( !$handle->isValid() ) { + continue; + } + + // Documentation pages are never translation pages + if ( $handle->isDoc() ) { + continue; + } + + $group = $handle->getGroup(); + if ( !$group instanceof WikiPageMessageGroup ) { + continue; + } + + $language = $handle->getCode(); + + // Ignore pages such as Translations:Page/unit without language code + if ( (string)$language === '' ) { + continue; + } + + // Update the page only once if source and destination units + // belong to the same page + if ( $group !== $groupLast ) { + $groupLast = $group; + $page = TranslatablePage::newFromTitle( $group->getTitle() ); + self::updateTranslationPage( $page, $language, $user, 0, $reason ); + } + } + } + + /** + * Hook to update source and destination translation pages on moving translation units * Hook: TitleMoveComplete + * + * Only run in versions of mediawiki before 1.35; in 1.35+, ::onMovePageTranslationUnits is used + * * @since 2014.08 * @param Title $ot * @param Title $nt @@ -1254,14 +1378,14 @@ class PageTranslationHooks { * Hook to update translation page on deleting a translation unit * Hook: ArticleDeleteComplete * @since 2016.05 - * @param WikiPage &$unit - * @param User &$user + * @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, + 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 @@ -1292,36 +1416,44 @@ class PageTranslationHooks { $langCode = $handle->getCode(); $targetPage = $target->getSubpage( $langCode )->getPrefixedText(); - if ( !isset( $queuedPages[ $targetPage ] ) ) { - $queuedPages[ $targetPage ] = true; - $fname = __METHOD__; + if ( isset( $queuedPages[ $targetPage ] ) ) { + return; + } - $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function () use ( $dbw, $queuedPages, $targetPage, - $target, $handle, $langCode, $user, $reason, $fname - ) { - $dbw->startAtomic( $fname ); + $queuedPages[ $targetPage ] = true; + $fname = __METHOD__; - $page = TranslatablePage::newFromTitle( $target ); + $dbw = wfGetDB( DB_MASTER ); + $callback = function () use ( + $dbw, $queuedPages, $targetPage, $target, $handle, $langCode, $user, $reason, $fname + ) { + $dbw->startAtomic( $fname ); - MessageGroupStats::forItem( - $page->getMessageGroupId(), - $langCode, - MessageGroupStats::FLAG_NO_CACHE - ); + $page = TranslatablePage::newFromTitle( $target ); - if ( !$handle->isDoc() ) { - // Assume that $user and $reason for the first deletion is the same for all - self::updateTranslationPage( $page, $langCode, $user, 0, $reason ); - } + 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 ] ); + // 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 ); - } ); + $dbw->endAtomic( $fname ); + }; + + if ( is_callable( [ $dbw, 'onTransactionCommitOrIdle' ] ) ) { + $dbw->onTransactionCommitOrIdle( $callback, __METHOD__ ); + } else { + $dbw->onTransactionIdle( $callback, __METHOD__ ); } } } diff --git a/MLEB/Translate/tag/SpecialPageMigration.php b/MLEB/Translate/tag/SpecialPageMigration.php index a0d1321b..462f0993 100644 --- a/MLEB/Translate/tag/SpecialPageMigration.php +++ b/MLEB/Translate/tag/SpecialPageMigration.php @@ -14,7 +14,7 @@ class SpecialPageMigration extends SpecialPage { } protected function getGroupName() { - return 'wiki'; + return 'translation'; } public function getDescription() { @@ -22,7 +22,6 @@ class SpecialPageMigration extends SpecialPage { } public function execute( $par ) { - $request = $this->getRequest(); $output = $this->getOutput(); $this->setHeaders(); $this->checkPermissions(); @@ -32,8 +31,7 @@ class SpecialPageMigration extends SpecialPage { 'ext.translate.special.pagemigration.styles', 'jquery.uls.grid' ] ); - # Get request data from, e.g. - $param = $request->getText( 'param' ); + # Do stuff # ... $out = ''; @@ -44,12 +42,14 @@ class SpecialPageMigration extends SpecialPage { [ 'class' => 'mw-tpm-sp-error__message five columns hide' ] ); $out .= Html::closeElement( 'div' ); $out .= Html::openElement( 'form', [ 'class' => 'mw-tpm-sp-form row', - 'id' => 'mw-tpm-sp-primary-form' ] ); + 'id' => 'mw-tpm-sp-primary-form', 'action' => '' ] ); $out .= Html::element( 'input', [ 'id' => 'pm-summary', 'type' => 'hidden', 'value' => $this->msg( 'pm-summary-import' )->inContentLanguage()->text() ] ); $out .= "\n"; $out .= Html::element( 'input', [ 'id' => 'title', 'class' => 'mw-searchInput mw-ui-input', - 'placeholder' => $this->msg( 'pm-pagetitle-placeholder' )->text() ] ); + 'data-mw-searchsuggest' => FormatJson::encode( [ + 'wrapAsLink' => false + ] ), 'placeholder' => $this->msg( 'pm-pagetitle-placeholder' )->text() ] ); $out .= "\n"; $out .= Html::element( 'input', [ 'id' => 'action-import', 'class' => 'mw-ui-button mw-ui-progressive', 'type' => 'button', diff --git a/MLEB/Translate/tag/SpecialPagePreparation.php b/MLEB/Translate/tag/SpecialPagePreparation.php index cd854e06..bdaf5dbf 100644 --- a/MLEB/Translate/tag/SpecialPagePreparation.php +++ b/MLEB/Translate/tag/SpecialPagePreparation.php @@ -14,7 +14,7 @@ class SpecialPagePreparation extends SpecialPage { } protected function getGroupName() { - return 'wiki'; + return 'translation'; } public function execute( $par ) { diff --git a/MLEB/Translate/tag/SpecialPageTranslation.php b/MLEB/Translate/tag/SpecialPageTranslation.php index 98eb5d6a..df9aeb5b 100644 --- a/MLEB/Translate/tag/SpecialPageTranslation.php +++ b/MLEB/Translate/tag/SpecialPageTranslation.php @@ -8,6 +8,8 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Revision\RevisionRecord; + /** * A special page for marking revisions of pages for translation. * @@ -18,6 +20,9 @@ * @ingroup SpecialPage PageTranslation */ class SpecialPageTranslation extends SpecialPage { + private const LATEST_SYNTAX_VERSION = '1'; + private const DEFAULT_SYNTAX_VERSION = '1'; + public function __construct() { parent::__construct( 'PageTranslation' ); } @@ -27,7 +32,7 @@ class SpecialPageTranslation extends SpecialPage { } protected function getGroupName() { - return 'pagetools'; + return 'translation'; } public function execute( $parameters ) { @@ -42,6 +47,7 @@ class SpecialPageTranslation extends SpecialPage { $out = $this->getOutput(); $out->addModules( 'ext.translate.special.pagetranslation' ); $out->addHelpLink( 'Help:Extension:Translate/Page_translation_example' ); + $out->enableOOUI(); if ( $target === '' ) { $this->listPages(); @@ -80,7 +86,7 @@ class SpecialPageTranslation extends SpecialPage { // On GET requests, show form which has token if ( !$request->wasPosted() ) { if ( $action === 'unlink' ) { - $this->showUnlinkConfirmation( $title, $target ); + $this->showUnlinkConfirmation( $title ); } else { $params = [ 'do' => $action, @@ -112,11 +118,19 @@ class SpecialPageTranslation extends SpecialPage { $entry->publish( $logid ); } - $this->listPages(); - + // Defer stats purging of parent aggregate groups. Shared groups can contain other + // groups as well, which we do not need to update. We could filter non-aggregate + // groups out, or use MessageGroups::getParentGroups, though it has an inconvenient + // return value format for this use case. $group = MessageGroups::getGroup( $id ); - $parents = MessageGroups::getSharedGroups( $group ); - MessageGroupStats::clearGroup( $parents ); + $sharedGroupIds = MessageGroups::getSharedGroups( $group ); + if ( $sharedGroupIds !== [] ) { + $job = MessageGroupStatsRebuildJob::newRefreshGroupsJob( $sharedGroupIds ); + JobQueueGroup::singleton()->push( $job ); + } + + // Show updated page with a notice + $this->listPages(); return; } @@ -201,16 +215,7 @@ class SpecialPageTranslation extends SpecialPage { return; } - $lastrev = $page->getMarkedTag(); - if ( $lastrev !== false && $lastrev === $revision ) { - $out->wrapWikiMsg( - '<div class="warningbox">$1</div>', - [ 'tpt-already-marked' ] - ); - $this->listPages(); - - return; - } + $firstMark = $page->getMarkedTag() === false; // This will modify the sections to include name property $error = false; @@ -226,25 +231,29 @@ class SpecialPageTranslation extends SpecialPage { } ); } - $err = $this->markForTranslation( $page, $sections ); + $setVersion = $firstMark || $request->getCheck( 'use-latest-syntax' ); + + $err = $this->markForTranslation( $page, $sections, $setVersion ); if ( $err ) { call_user_func_array( [ $out, 'addWikiMsg' ], $err ); } else { - $this->showSuccess( $page ); + $this->showSuccess( $page, $firstMark ); $this->listPages(); } return; } - $this->showPage( $page, $sections ); + $this->showPage( $page, $sections, $firstMark ); } /** + * Displays success message and other instructions after a page has been marked for translation. * @param TranslatablePage $page + * @param bool $firstMark true if it is the first time the page is being marked for translation. */ - public function showSuccess( TranslatablePage $page ) { + public function showSuccess( TranslatablePage $page, $firstMark = false ) { $titleText = $page->getTitle()->getPrefixedText(); $num = $this->getLanguage()->formatNum( $page->getParse()->countSections() ); $link = SpecialPage::getTitleFor( 'Translate' )->getFullURL( [ @@ -258,9 +267,15 @@ class SpecialPageTranslation extends SpecialPage { [ 'tpt-saveok', $titleText, $num, $link ] ); + // If the page is being marked for translation for the first time + // add a link to Special:PageMigration. + if ( $firstMark ) { + $this->getOutput()->addWikiMsg( 'tpt-saveok-first' ); + } + // If TranslationNotifications is installed, and the user can notify // translators, add a convenience link. - if ( method_exists( 'SpecialNotifyTranslators', 'execute' ) && + if ( method_exists( SpecialNotifyTranslators::class, 'execute' ) && $this->getUser()->isAllowed( SpecialNotifyTranslators::$right ) ) { $link = SpecialPage::getTitleFor( 'NotifyTranslators' )->getFullURL( @@ -370,39 +385,52 @@ class SpecialPageTranslation extends SpecialPage { } /** - * @param array $in - * @return array + * Classify a list of pages and amend them with additional metadata. + * + * @param array[] $pages + * @return array[] + * @phan-return array{proposed:array[],active:array[],broken:array[],outdated:array[]} */ - protected function classifyPages( array $in ) { + private function classifyPages( array $pages ): array { + // Preload stuff for performance + $messageGroupIdsForPreload = []; + foreach ( $pages as $i => $page ) { + $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] ); + $messageGroupIdsForPreload[] = $id; + $pages[$i]['groupid'] = $id; + } + TranslateMetadata::preloadGroups( $messageGroupIdsForPreload ); + $out = [ - 'proposed' => [], + // The ideal state for pages: marked and up to date 'active' => [], + 'proposed' => [], + 'outdated' => [], 'broken' => [], - 'discouraged' => [], ]; - foreach ( $in as $index => $page ) { + foreach ( $pages as $page ) { + $group = MessageGroups::getGroup( $page['groupid'] ); + $page['discouraged'] = MessageGroups::getPriority( $group ) === 'discouraged'; + $page['version'] = TranslateMetadata::getWithDefaultValue( + $page['groupid'], 'version', self::DEFAULT_SYNTAX_VERSION + ); + if ( !isset( $page['tp:mark'] ) ) { // Never marked, check that the latest version is ready if ( $page['tp:tag'] === $page['latest'] ) { - $out['proposed'][$index] = $page; + $out['proposed'][] = $page; } // Otherwise ignore such pages } elseif ( $page['tp:tag'] === $page['latest'] ) { - // Marked and latest version if fine - $out['active'][$index] = $page; + if ( $page['tp:mark'] === $page['tp:tag'] ) { + // Marked and latest version is fine + $out['active'][] = $page; + } else { + $out['outdated'][] = $page; + } } else { - // Marked but latest version if not fine - $out['broken'][$index] = $page; - } - } - - // broken and proposed take preference over discouraged status - foreach ( $out['active'] as $index => $page ) { - $id = TranslatablePage::getMessageGroupIdFromTitle( $page['title'] ); - $group = MessageGroups::getGroup( $id ); - if ( MessageGroups::getPriority( $group ) === 'discouraged' ) { - $out['discouraged'][$index] = $page; - unset( $out['active'][$index] ); + // Marked but latest version is not fine + $out['broken'][] = $page; } } @@ -433,73 +461,34 @@ class SpecialPageTranslation extends SpecialPage { if ( count( $pages ) ) { $out->wrapWikiMsg( '== $1 ==', 'tpt-new-pages-title' ); $out->addWikiMsg( 'tpt-new-pages', count( $pages ) ); - $out->addHTML( '<ol>' ); - foreach ( $pages as $page ) { - $link = Linker::link( $page['title'] ); - $acts = $this->actionLinks( $page, 'proposed' ); - $out->addHTML( "<li>$link $acts</li>" ); - } - $out->addHTML( '</ol>' ); - } - - $pages = $types['active']; - if ( count( $pages ) ) { - $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' ); - $out->addWikiMsg( 'tpt-old-pages', count( $pages ) ); - $out->addHTML( '<ol>' ); - foreach ( $pages as $page ) { - $link = Linker::link( $page['title'] ); - if ( $page['tp:mark'] !== $page['tp:tag'] ) { - $link = "<strong>$link</strong>"; - } - - $acts = $this->actionLinks( $page, 'active' ); - $out->addHTML( "<li>$link $acts</li>" ); - } - $out->addHTML( '</ol>' ); + $out->addHTML( $this->getPageList( $pages, 'proposed' ) ); } $pages = $types['broken']; if ( count( $pages ) ) { $out->wrapWikiMsg( '== $1 ==', 'tpt-other-pages-title' ); $out->addWikiMsg( 'tpt-other-pages', count( $pages ) ); - $out->addHTML( '<ol>' ); - foreach ( $pages as $page ) { - $link = Linker::link( $page['title'] ); - $acts = $this->actionLinks( $page, 'broken' ); - $out->addHTML( "<li>$link $acts</li>" ); - } - $out->addHTML( '</ol>' ); + $out->addHTML( $this->getPageList( $pages, 'broken' ) ); } - $pages = $types['discouraged']; + $pages = $types['outdated']; if ( count( $pages ) ) { - $out->wrapWikiMsg( '== $1 ==', 'tpt-discouraged-pages-title' ); - $out->addWikiMsg( 'tpt-discouraged-pages', count( $pages ) ); - $out->addHTML( '<ol>' ); - foreach ( $pages as $page ) { - $link = Linker::link( $page['title'] ); - if ( $page['tp:mark'] !== $page['tp:tag'] ) { - $link = "<strong>$link</strong>"; - } + $out->wrapWikiMsg( '== $1 ==', 'tpt-outdated-pages-title' ); + $out->addWikiMsg( 'tpt-outdated-pages', count( $pages ) ); + $out->addHTML( $this->getPageList( $pages, 'outdated' ) ); + } - $acts = $this->actionLinks( $page, 'discouraged' ); - $out->addHTML( "<li>$link $acts</li>" ); - } - $out->addHTML( '</ol>' ); + $pages = $types['active']; + if ( count( $pages ) ) { + $out->wrapWikiMsg( '== $1 ==', 'tpt-old-pages-title' ); + $out->addWikiMsg( 'tpt-old-pages', count( $pages ) ); + $out->addHTML( $this->getPageList( $pages, 'active' ) ); } } - /** - * @param array $page - * @param string $type - * @return string - */ - protected function actionLinks( array $page, $type ) { + private function actionLinks( array $page, string $type ): string { $actions = []; - /** - * @var Title $title - */ + /** @var Title $title */ $title = $page['title']; $user = $this->getUser(); @@ -507,8 +496,9 @@ class SpecialPageTranslation extends SpecialPage { $js = [ 'class' => 'mw-translate-jspost' ]; if ( $user->isAllowed( 'pagetranslation' ) ) { - $pending = $type === 'active' && $page['latest'] !== $page['tp:mark']; - if ( $type === 'proposed' || $pending ) { + // Enable re-marking of all pages to allow changing of priority languages + // or migration to the new syntax version + if ( $type !== 'broken' ) { $actions[] = Linker::linkKnown( $this->getPageTitle(), $this->msg( 'tpt-rev-mark' )->escaped(), @@ -521,31 +511,31 @@ class SpecialPageTranslation extends SpecialPage { ); } - if ( $type === 'active' ) { - $actions[] = Linker::linkKnown( - $this->getPageTitle(), - $this->msg( 'tpt-rev-discourage' )->escaped(), - [ 'title' => $this->msg( 'tpt-rev-discourage-tooltip' )->text() ] + $js, - [ - 'do' => 'discourage', - 'target' => $title->getPrefixedText(), - 'revision' => -1, - ] - ); - } elseif ( $type === 'discouraged' ) { - $actions[] = Linker::linkKnown( - $this->getPageTitle(), - $this->msg( 'tpt-rev-encourage' )->escaped(), - [ 'title' => $this->msg( 'tpt-rev-encourage-tooltip' )->text() ] + $js, - [ - 'do' => 'encourage', - 'target' => $title->getPrefixedText(), - 'revision' => -1, - ] - ); - } - if ( $type !== 'proposed' ) { + if ( $page['discouraged'] ) { + $actions[] = Linker::linkKnown( + $this->getPageTitle(), + $this->msg( 'tpt-rev-encourage' )->escaped(), + [ 'title' => $this->msg( 'tpt-rev-encourage-tooltip' )->text() ] + $js, + [ + 'do' => 'encourage', + 'target' => $title->getPrefixedText(), + 'revision' => -1, + ] + ); + } else { + $actions[] = Linker::linkKnown( + $this->getPageTitle(), + $this->msg( 'tpt-rev-discourage' )->escaped(), + [ 'title' => $this->msg( 'tpt-rev-discourage-tooltip' )->text() ] + $js, + [ + 'do' => 'discourage', + 'target' => $title->getPrefixedText(), + 'revision' => -1, + ] + ); + } + $actions[] = Linker::linkKnown( $this->getPageTitle(), $this->msg( 'tpt-rev-unmark' )->escaped(), @@ -563,13 +553,7 @@ class SpecialPageTranslation extends SpecialPage { return ''; } - $flattened = $this->getLanguage()->semicolonList( $actions ); - - return Html::rawElement( - 'span', - [ 'class' => 'mw-tpt-actions' ], - $this->msg( 'parentheses' )->rawParams( $flattened )->escaped() - ); + return '<div>' . $this->getLanguage()->pipeList( $actions ) . '</div>'; } /** @@ -584,30 +568,38 @@ class SpecialPageTranslation extends SpecialPage { $sections = $parse->getSectionsForSave( $highest ); foreach ( $sections as $s ) { + if ( preg_match( '~[_/]~', $s->id ) ) { + $this->getOutput()->addElement( + 'p', + [ 'class' => 'errorbox' ], + $this->msg( 'tpt-invalid' )->params( $s->id )->text() + ); + $error = true; + } + // We need to do checks for both new and existing sections. // Someone might have tampered with the page source adding // duplicate or invalid markers. - if ( isset( $usedNames[$s->id] ) ) { - $this->getOutput()->addWikiMsg( 'tpt-duplicate', $s->id ); + $usedNames[$s->id] = ( $usedNames[$s->id] ?? 0 ) + 1; + $s->name = $s->id; + } + foreach ( $usedNames as $name => $count ) { + if ( $count > 1 ) { + // Only show error once per duplicated translation unit + $this->getOutput()->addElement( + 'p', + [ 'class' => 'errorbox' ], + $this->msg( 'tpt-duplicate' )->params( $name )->text() + ); $error = true; } - $usedNames[$s->id] = true; - $s->name = $s->id; } - return $sections; } - /** - * Displays the sections and changes for the user to review - * @param TranslatablePage $page - * @param TPSection[] $sections - */ - public function showPage( TranslatablePage $page, array $sections ) { + private function showPage( TranslatablePage $page, array $sections, bool $firstMark ): void { $out = $this->getOutput(); - $out->setSubtitle( Linker::link( $page->getTitle() ) ); - $out->addWikiMsg( 'tpt-showpage-intro' ); $formParams = [ @@ -643,12 +635,18 @@ class SpecialPageTranslation extends SpecialPage { $s->type = $defaultChecked ? $s->type : 'new'; // Checkbox for page title optional translation - $this->getOutput()->addHTML( Xml::checkLabel( - $this->msg( 'tpt-translate-title' )->text(), - 'translatetitle', - 'mw-translate-title', - $defaultChecked - ) ); + $checkBox = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'name' => 'translatetitle', + 'selected' => $defaultChecked, + ] ), + [ + 'label' => $this->msg( 'tpt-translate-title' )->text(), + 'align' => 'inline', + 'classes' => [ 'mw-tpt-m-vertical' ] + ] + ); + $out->addHTML( $checkBox->toString() ); } if ( $s->type === 'new' ) { @@ -674,13 +672,18 @@ class SpecialPageTranslation extends SpecialPage { $diff->showDiffStyle(); $id = "tpt-sect-{$s->id}-action-nofuzzy"; - $checkLabel = Xml::checkLabel( - $this->msg( 'tpt-action-nofuzzy' )->text(), - $id, - $id, - false + $checkLabel = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'name' => $id, + 'selected' => false, + ] ), + [ + 'label' => $this->msg( 'tpt-action-nofuzzy' )->text(), + 'align' => 'inline', + 'classes' => [ 'mw-tpt-m-vertical' ] + ] ); - $text = $checkLabel . $text; + $text = $checkLabel->toString() . $text; } else { $text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() ); } @@ -700,9 +703,7 @@ class SpecialPageTranslation extends SpecialPage { $hasChanges = true; $out->wrapWikiMsg( '==$1==', 'tpt-sections-deleted' ); - /** - * @var TPSection $s - */ + /** @var TPSection $s */ foreach ( $deletedSections as $s ) { $name = $this->msg( 'tpt-section-deleted', $s->id )->escaped(); $text = TranslateUtils::convertWhiteSpaceToHTML( $s->getText() ); @@ -752,65 +753,98 @@ class SpecialPageTranslation extends SpecialPage { $out->wrapWikiMsg( '<div class="successbox">$1</div>', 'tpt-mark-nochanges' ); } + $version = TranslateMetadata::getWithDefaultValue( + $page->getMessageGroupId(), 'version', self::DEFAULT_SYNTAX_VERSION + ); $this->priorityLanguagesForm( $page ); - $out->addHTML( - Xml::submitButton( $this->msg( 'tpt-submit' )->text() ) . - Xml::closeElement( 'form' ) + $this->syntaxVersionForm( $version, $firstMark ); + + $submitButton = new OOUI\FieldLayout( + new OOUI\ButtonInputWidget( [ + 'label' => $this->msg( 'tpt-submit' )->text(), + 'type' => 'submit', + 'flags' => [ 'primary', 'progressive' ], + ] ), + [ + 'label' => null, + 'align' => 'top', + ] ); - } - /** - * @param TranslatablePage $page - */ - protected function priorityLanguagesForm( TranslatablePage $page ) { - global $wgContLang; + $out->addHTML( $submitButton->toString() ); + $out->addHTML( '</form>' ); + } + private function priorityLanguagesForm( TranslatablePage $page ): void { $groupId = $page->getMessageGroupId(); + + $form = new OOUI\FieldsetLayout( [ + 'items' => [ + new OOUI\FieldLayout( + new OOUI\TextInputWidget( [ + 'name' => 'prioritylangs', + 'value' => TranslateMetadata::get( $groupId, 'prioritylangs' ), + 'inputId' => 'tpt-prioritylangs', + 'dir' => 'ltr', + ] ), + [ + 'label' => $this->msg( 'tpt-select-prioritylangs' )->text(), + 'align' => 'top', + ] + ), + new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'name' => 'forcelimit', + 'selected' => TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on', + ] ), + [ + 'label' => $this->msg( 'tpt-select-prioritylangs-force' )->text(), + 'align' => 'inline', + ] + ), + new OOUI\FieldLayout( + new OOUI\TextInputWidget( [ + 'name' => 'priorityreason', + ] ), + [ + 'label' => $this->msg( 'tpt-select-prioritylangs-reason' )->text(), + 'align' => 'top', + ] + ), + + ] + ] ); + $this->getOutput()->wrapWikiMsg( '==$1==', 'tpt-sections-prioritylangs' ); + $this->getOutput()->addHTML( $form->toString() ); + } - $langSelector = Xml::languageSelector( - $wgContLang->getCode(), - false, - $this->getLanguage()->getCode() - ); + private function syntaxVersionForm( string $version, bool $firstMark ): void { + $out = $this->getOutput(); - $hLangs = Xml::inputLabelSep( - $this->msg( 'tpt-select-prioritylangs' )->text(), - 'prioritylangs', // name - 'tpt-prioritylangs', // id - 50, - TranslateMetadata::get( $groupId, 'prioritylangs' ) - ); + if ( $version === self::LATEST_SYNTAX_VERSION || $firstMark ) { + return; + } - $hForce = Xml::checkLabel( - $this->msg( 'tpt-select-prioritylangs-force' )->text(), - 'forcelimit', // name - 'tpt-priority-forcelimit', // id - TranslateMetadata::get( $groupId, 'priorityforce' ) === 'on' + $out->wrapWikiMsg( '==$1==', 'tpt-sections-syntaxversion' ); + $out->addWikiMsg( + 'tpt-syntaxversion-text', + '<code>' . wfEscapeWikiText( '<span lang="en" dir="ltr">...</span>' ) . '</code>', + '<code>' . wfEscapeWikiText( '<translate nowrap>...</translate>' ) . '</code>' ); - $hReason = Xml::inputLabelSep( - $this->msg( 'tpt-select-prioritylangs-reason' )->text(), - 'priorityreason', // name - 'tpt-priority-reason', // id - 50, // size - TranslateMetadata::get( $groupId, 'priorityreason' ) + $checkBox = new OOUI\FieldLayout( + new OOUI\CheckboxInputWidget( [ + 'name' => 'use-latest-syntax' + ] ), + [ + 'label' => $out->msg( 'tpt-syntaxversion-label' )->text(), + 'align' => 'inline', + ] ); - $this->getOutput()->addHTML( - '<table>' . - '<tr>' . - "<td class='mw-label'>$hLangs[0]</td>" . - "<td class='mw-input'>$hLangs[1]$langSelector[1]</td>" . - '</tr>' . - "<tr><td></td><td class='mw-inout'>$hForce</td></tr>" . - '<tr>' . - "<td class='mw-label'>$hReason[0]</td>" . - "<td class='mw-input'>$hReason[1]</td>" . - '</tr>' . - '</table>' - ); + $out->addHTML( $checkBox->toString() ); } /** @@ -822,9 +856,14 @@ class SpecialPageTranslation extends SpecialPage { * - Invalidates caches * @param TranslatablePage $page * @param TPSection[] $sections + * @param bool $updateVersion * @return array|bool */ - public function markForTranslation( TranslatablePage $page, array $sections ) { + protected function markForTranslation( + TranslatablePage $page, + array $sections, + bool $updateVersion + ) { // Add the section markers to the source page $wikiPage = WikiPage::factory( $page->getTitle() ); $content = ContentHandler::makeContent( @@ -842,23 +881,42 @@ class SpecialPageTranslation extends SpecialPage { return [ 'tpt-edit-failed', $status->getWikiText() ]; } - $newrevision = $status->value['revision']; + if ( version_compare( MW_VERSION, '1.35', '>=' ) ) { + // MW 1.35+ + // Cannot use array_key_exists with DeprecatablePropertyArray, and + // $status->value['revision-record'] can be null, so isset doesn't + // work either + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable + $newRevisionRecord = $status->value['revision-record']; + } else { + // @phan-suppress-next-line PhanTypeArraySuspiciousNullable + $newRevision = $status->value['revision']; + if ( $newRevision instanceof Revision ) { + $newRevisionRecord = $newRevision->getRevisionRecord(); + } else { + $newRevisionRecord = null; + } + } - // In theory it is either null or Revision object, - // never revision object with null id, but who knows - if ( $newrevision instanceof Revision ) { - $newrevision = $newrevision->getId(); + // In theory it is either null or RevisionRecord object, + // not a RevisionRecord object with null id, but who knows + if ( $newRevisionRecord instanceof RevisionRecord ) { + $newRevisionId = $newRevisionRecord->getId(); + } else { + $newRevisionId = null; } - if ( $newrevision === null ) { - // Probably a no-change edit, so no new revision was assigned. - // Get the latest revision manually - $newrevision = $page->getTitle()->getLatestRevID(); + // Probably a no-change edit, so no new revision was assigned. + // Get the latest revision manually + // Could also occur on the off chance $newRevisionRecord->getId() returns null + if ( $newRevisionId === null ) { + $newRevisionId = $page->getTitle()->getLatestRevID(); } $inserts = []; $changed = []; - $maxid = (int)TranslateMetadata::get( $page->getMessageGroupId(), 'maxid' ); + $groupId = $page->getMessageGroupId(); + $maxid = (int)TranslateMetadata::get( $groupId, 'maxid' ); $pageId = $page->getTitle()->getArticleID(); /** @@ -888,11 +946,17 @@ class SpecialPageTranslation extends SpecialPage { __METHOD__ ); $dbw->insert( 'translate_sections', $inserts, __METHOD__ ); - TranslateMetadata::set( $page->getMessageGroupId(), 'maxid', $maxid ); + TranslateMetadata::set( $groupId, 'maxid', $maxid ); + if ( $updateVersion ) { + TranslateMetadata::set( $groupId, 'version', self::LATEST_SYNTAX_VERSION ); + } - $page->addMarkedTag( $newrevision ); + $page->addMarkedTag( $newRevisionId ); MessageGroups::singleton()->recache(); + $group = $page->getMessageGroup(); + $newKeys = $group->makeGroupKeys( $changed ); + MessageIndex::singleton()->storeInterim( $group, $newKeys ); $job = TranslationsUpdateJob::newFromPage( $page, $sections ); JobQueueGroup::singleton()->push( $job ); @@ -903,7 +967,7 @@ class SpecialPageTranslation extends SpecialPage { $entry->setPerformer( $this->getUser() ); $entry->setTarget( $page->getTitle() ); $entry->setParameters( [ - 'revision' => $newrevision, + 'revision' => $newRevisionId, 'changed' => count( $changed ), ] ); $logid = $entry->insert(); @@ -977,8 +1041,39 @@ class SpecialPageTranslation extends SpecialPage { * @since 2014.09 */ public static function getStrippedSourcePageText( TPParse $parse ) { - $text = $parse->getTranslationPageText( [] ); + $text = $parse->getTranslationPageText( null ); $text = preg_replace( '~<languages\s*/>\n?~s', '', $text ); return $text; } + + private function getPageList( array $pages, string $type ): string { + $items = []; + + foreach ( $pages as $page ) { + $link = Linker::link( $page['title'] ); + $acts = $this->actionLinks( $page, $type ); + $tags = []; + if ( $page['discouraged'] ) { + $tags[] = $this->msg( 'tpt-tag-discouraged' )->escaped(); + } + if ( $type !== 'proposed' && $page['version'] !== self::LATEST_SYNTAX_VERSION ) { + $tags[] = $this->msg( 'tpt-tag-oldsyntax' )->escaped(); + } + + $tagList = ''; + if ( $tags ) { + $tagList = Html::rawElement( + 'span', + [ 'class' => 'mw-tpt-actions' ], + $this->msg( 'parentheses' )->rawParams( + $this->getLanguage()->pipeList( $tags ) + )->escaped() + ); + } + + $items[] = "<li>$link $tagList $acts</li>"; + } + + return '<ol>' . implode( "", $items ) . '</ol>'; + } } diff --git a/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php b/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php index 8fe011dc..9ef78028 100644 --- a/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php +++ b/MLEB/Translate/tag/SpecialPageTranslationDeletePage.php @@ -7,6 +7,8 @@ * @license GPL-2.0-or-later */ +use MediaWiki\MediaWikiServices; + /** * Special page which enables deleting translations of translatable pages * @@ -39,8 +41,9 @@ class SpecialPageTranslationDeletePage extends SpecialPage { /// Contains the language code if we are working with translation page protected $code; - protected $sectionPages; - + /** + * @var Title[] + */ protected $translationPages; public function __construct() { @@ -56,6 +59,8 @@ class SpecialPageTranslationDeletePage extends SpecialPage { } public function execute( $par ) { + $this->addhelpLink( 'Help:Deletion_and_undeletion' ); + $request = $this->getRequest(); $par = (string)$par; @@ -147,15 +152,14 @@ class SpecialPageTranslationDeletePage extends SpecialPage { throw new ErrorPageError( 'nopagetitle', 'nopagetext' ); } - $permissionErrors = $this->title->getUserPermissionsErrors( 'delete', $this->getUser() ); + $permissionErrors = MediaWikiServices::getInstance()->getPermissionManager() + ->getPermissionErrors( 'delete', $this->getUser(), $this->title ); if ( count( $permissionErrors ) ) { throw new PermissionsError( 'delete', $permissionErrors ); } # Check for database lock - if ( wfReadOnly() ) { - throw new ReadOnlyError; - } + $this->checkReadOnly(); // Let the caller know it's safe to continue return true; @@ -176,22 +180,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage { protected function showForm() { $this->getOutput()->addWikiMsg( 'pt-deletepage-intro' ); - $formDescriptor = [ - 'wpTitle' => [ - 'type' => 'text', - 'name' => 'wpTitle', - 'label' => $this->msg( 'pt-deletepage-current' )->text(), - 'size' => 30, - 'default' => $this->text, - ], - 'wpReason' => [ - 'type' => 'text', - 'name' => 'wpReason', - 'label' => $this->msg( 'pt-deletepage-reason' )->text(), - 'size' => 60, - 'default' => $this->reason, - ] - ]; + $formDescriptor = $this->getCommonFormFields(); $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); $htmlForm @@ -212,14 +201,14 @@ class SpecialPageTranslationDeletePage extends SpecialPage { protected function showConfirmation() { $out = $this->getOutput(); $count = 0; + $subpageCount = 0; $out->addWikiMsg( 'pt-deletepage-intro' ); $out->wrapWikiMsg( '== $1 ==', 'pt-deletepage-list-pages' ); if ( !$this->singleLanguage() ) { $count++; - TranslateUtils::addWikiTextAsInterface( - $out, + $out->addWikiTextAsInterface( $this->getChangeLine( $this->title ) ); } @@ -231,7 +220,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage { $count++; $lines[] = $this->getChangeLine( $old ); } - TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) ); + $this->listPages( $out, $lines ); $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-section' ); $sectionPages = $this->getSectionPages(); @@ -240,50 +229,39 @@ class SpecialPageTranslationDeletePage extends SpecialPage { $count++; $lines[] = $this->getChangeLine( $old ); } - TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) ); + $this->listPages( $out, $lines ); - $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' ); - $subpages = $this->getSubpages(); - $lines = []; - foreach ( $subpages as $old ) { - if ( TranslatablePage::isTranslationPage( $old ) ) { - continue; - } + if ( TranslateUtils::allowsSubpages( $this->title ) ) { + $out->wrapWikiMsg( '=== $1 ===', 'pt-deletepage-list-other' ); + $subpages = $this->getSubpages(); + $lines = []; + foreach ( $subpages as $old ) { + if ( TranslatablePage::isTranslationPage( $old ) ) { + continue; + } - if ( $this->doSubpages ) { - $count++; + $subpageCount++; + $lines[] = $this->getChangeLine( $old ); } - - $lines[] = $this->getChangeLine( $old, $this->doSubpages ); + $this->listPages( $out, $lines ); } - TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) ); - TranslateUtils::addWikiTextAsInterface( $out, "----\n" ); - $out->addWikiMsg( 'pt-deletepage-list-count', $this->getLanguage()->formatNum( $count ) ); + $totalPageCount = $count + $subpageCount; - $formDescriptor = [ - 'wpTitle' => [ - 'type' => 'text', - 'name' => 'wpTitle', - 'label' => $this->msg( 'pt-deletepage-current' )->text(), - 'size' => 30, - 'default' => $this->text, - 'readonly' => true, - ], - 'wpReason' => [ - 'type' => 'text', - 'name' => 'wpReason', - 'label' => $this->msg( 'pt-deletepage-reason' )->text(), - 'size' => 60, - 'default' => $this->reason, - ], - 'subpages' => [ - 'type' => 'check', - 'name' => 'subpages', - 'id' => 'mw-subpages', - 'label' => $this->msg( 'pt-deletepage-subpages' )->text(), - 'default' => $this->doSubpages, - ] + $out->addWikiTextAsInterface( "----\n" ); + $out->addWikiMsg( + 'pt-deletepage-list-count', + $this->getLanguage()->formatNum( $totalPageCount ), + $this->getLanguage()->formatNum( $subpageCount ) + ); + + $formDescriptor = $this->getCommonFormFields(); + $formDescriptor['subpages'] = [ + 'type' => 'check', + 'name' => 'subpages', + 'id' => 'mw-subpages', + 'label' => $this->msg( 'pt-deletepage-subpages' )->text(), + 'default' => $this->doSubpages, ]; $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); @@ -310,15 +288,10 @@ class SpecialPageTranslationDeletePage extends SpecialPage { /** * @param Title $title - * @param bool $enabled * @return string One line of wikitext, without trailing newline. */ - protected function getChangeLine( $title, $enabled = true ) { - if ( $enabled ) { - return '* ' . $title->getPrefixedText(); - } else { - return '* <s>' . $title->getPrefixedText() . '</s>'; - } + protected function getChangeLine( $title ) { + return '* ' . $title->getPrefixedText(); } protected function performAction() { @@ -366,6 +339,16 @@ class SpecialPageTranslationDeletePage extends SpecialPage { } } + if ( !$this->singleLanguage() ) { + $jobs[$this->title->getPrefixedText()] = TranslateDeleteJob::newJob( + $this->title, + $base, + !$this->singleLanguage(), + $user, + $this->reason + ); + } + JobQueueGroup::singleton()->push( $jobs ); $cache = wfGetCache( CACHE_DB ); @@ -393,22 +376,21 @@ class SpecialPageTranslationDeletePage extends SpecialPage { TranslateMetadata::set( $groupId, 'priorityforce', false ); TranslateMetadata::set( $groupId, 'priorityreason', false ); // remove the page from aggregate groups, if present in any of them. - $groups = MessageGroups::getAllGroups(); - foreach ( $groups as $group ) { - if ( $group instanceof AggregateMessageGroup ) { - $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' ); - if ( $subgroups !== false ) { - $subgroups = explode( ',', $subgroups ); + $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class ); + TranslateMetadata::preloadGroups( array_keys( $aggregateGroups ) ); + foreach ( $aggregateGroups as $group ) { + $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' ); + if ( $subgroups !== false ) { + $subgroups = explode( ',', $subgroups ); + $subgroups = array_flip( $subgroups ); + if ( isset( $subgroups[$groupId] ) ) { + unset( $subgroups[$groupId] ); $subgroups = array_flip( $subgroups ); - if ( isset( $subgroups[$groupId] ) ) { - unset( $subgroups[$groupId] ); - $subgroups = array_flip( $subgroups ); - TranslateMetadata::set( - $group->getId(), - 'subgroups', - implode( ',', $subgroups ) - ); - } + TranslateMetadata::set( + $group->getId(), + 'subgroups', + implode( ',', $subgroups ) + ); } } } @@ -416,7 +398,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage { /** * Returns all section pages, including those which are currently not active. - * @return Array of titles. + * @return Title[] */ protected function getSectionPages() { $code = $this->singleLanguage() ? $this->code : false; @@ -426,7 +408,7 @@ class SpecialPageTranslationDeletePage extends SpecialPage { /** * Returns only translation subpages. - * @return Array of titles. + * @return Title[] */ protected function getTranslationPages() { if ( $this->singleLanguage() ) { @@ -454,4 +436,32 @@ class SpecialPageTranslationDeletePage extends SpecialPage { protected function singleLanguage() { return $this->code !== ''; } + + protected function getCommonFormFields() { + return [ + 'wpTitle' => [ + 'type' => 'text', + 'name' => 'wpTitle', + 'label' => $this->msg( 'pt-deletepage-current' )->text(), + 'size' => 30, + 'default' => $this->text, + 'readonly' => true, + ], + 'wpReason' => [ + 'type' => 'text', + 'name' => 'wpReason', + 'label' => $this->msg( 'pt-deletepage-reason' )->text(), + 'size' => 60, + 'default' => $this->reason, + ] + ]; + } + + protected function listPages( OutputPage $out, array $lines ): void { + if ( $lines ) { + $out->addWikiTextAsInterface( implode( "\n", $lines ) ); + } else { + $out->addWikiMsg( 'pt-deletepage-list-no-pages' ); + } + } } diff --git a/MLEB/Translate/tag/SpecialPageTranslationMovePage.php b/MLEB/Translate/tag/SpecialPageTranslationMovePage.php index 452f65fb..87dd6fb2 100644 --- a/MLEB/Translate/tag/SpecialPageTranslationMovePage.php +++ b/MLEB/Translate/tag/SpecialPageTranslationMovePage.php @@ -7,6 +7,8 @@ * @license GPL-2.0-or-later */ +use MediaWiki\MediaWikiServices; + /** * Overrides Special:Movepage to to allow renaming a page translation page and * all related translations and derivative pages. @@ -17,11 +19,6 @@ class SpecialPageTranslationMovePage extends MovePageForm { // Basic form parameters both as text and as titles protected $newText, $oldText; - /** - * @var Title - */ - protected $newTitle, $oldTitle; - // Other form parameters /** * 'check' or 'perform' @@ -29,16 +26,6 @@ class SpecialPageTranslationMovePage extends MovePageForm { protected $subaction; /** - * There must be reason for everything. - */ - protected $reason; - - /** - * Allow skipping non-translation subpages. - */ - protected $moveSubpages; - - /** * @var TranslatablePage instance. */ protected $page; @@ -58,18 +45,6 @@ class SpecialPageTranslationMovePage extends MovePageForm { */ protected $sectionPages; - public function __construct() { - parent::__construct( 'Movepage' ); - } - - public function doesWrites() { - return true; - } - - public function isListed() { - return false; - } - /** * Partially copies from SpecialMovepage.php, because it cannot be * extended in other ways. @@ -80,14 +55,13 @@ class SpecialPageTranslationMovePage extends MovePageForm { public function execute( $par ) { $request = $this->getRequest(); $user = $this->getUser(); - - $par = is_null( $par ) ? '' : $par; // Title::newFromText expects strings only + $this->addHelpLink( 'Help:Extension:Translate/Move_translatable_page' ); // Yes, the use of getVal() and getText() is wanted, see bug T22365 $this->oldText = $request->getVal( 'wpOldTitle', $request->getVal( 'target', $par ) ); $this->newText = $request->getText( 'wpNewTitle' ); - $this->oldTitle = Title::newFromText( $this->oldText ); + $this->oldTitle = Title::newFromText( $this->oldText ?? '' ); $this->newTitle = Title::newFromText( $this->newText ); $this->reason = $request->getText( 'reason' ); @@ -125,7 +99,7 @@ class SpecialPageTranslationMovePage extends MovePageForm { } if ( $subaction === 'check' && $this->checkToken() && $request->wasPosted() ) { - $blockers = $this->checkMoveBlockers(); + $blockers = $this->checkMoveBlockers( $user ); if ( count( $blockers ) ) { $this->showErrors( $blockers ); $this->showForm( [] ); @@ -165,7 +139,8 @@ class SpecialPageTranslationMovePage extends MovePageForm { } // Check rights - $permErrors = $this->oldTitle->getUserPermissionsErrors( 'move', $this->getUser() ); + $permErrors = MediaWikiServices::getInstance()->getPermissionManager() + ->getPermissionErrors( 'move', $this->getUser(), $this->oldTitle ); if ( count( $permErrors ) ) { throw new PermissionsError( 'move', $permErrors ); } @@ -183,24 +158,28 @@ class SpecialPageTranslationMovePage extends MovePageForm { /** * Pretty-print the list of errors. - * @param array $errors Array with message key and parameters + * @param SplObjectStorage $errors Array with message key and parameters */ - protected function showErrors( array $errors ) { - if ( count( $errors ) ) { - $out = $this->getOutput(); - - $out->addHTML( Html::openElement( 'div', [ 'class' => 'error' ] ) ); - $out->addWikiMsg( - 'pt-movepage-blockers', - $this->getLanguage()->formatNum( count( $errors ) ) - ); - $s = ''; - foreach ( $errors as $error ) { - $s .= '* ' . wfMessage( ...$error )->plain() . "\n"; - } - TranslateUtils::addWikiTextAsInterface( $out, $s ); - $out->addHTML( '</div>' ); + protected function showErrors( SplObjectStorage $errors ) { + $out = $this->getOutput(); + + $out->addHtml( Html::openElement( 'div', [ 'class' => 'errorbox' ] ) ); + $out->addWikiMsg( + 'pt-movepage-blockers', + $this->getLanguage()->formatNum( count( $errors ) ) + ); + + // If there are many errors, for performance reasons we must parse them all at once + $s = ''; + $context = 'pt-movepage-error-placeholder'; + foreach ( $errors as $title ) { + // @phan-suppress-next-line PhanTypeSuspiciousStringExpression + $s .= "'''$title'''\n\n"; + $s .= $errors[ $title ]->getWikiText( false, $context ); } + + $out->addWikiTextAsInterface( $s ); + $out->addHtml( Html::closeElement( 'div' ) ); } /** @@ -234,13 +213,6 @@ class SpecialPageTranslationMovePage extends MovePageForm { 'label' => $this->msg( 'pt-movepage-reason' )->text(), 'size' => 45, 'default' => $this->reason, - ], - 'subpages' => [ - 'type' => 'check', - 'name' => 'subpages', - 'id' => 'mw-subpages', - 'label' => $this->msg( 'pt-movepage-subpages' )->text(), - 'default' => $this->moveSubpages, ] ]; @@ -295,46 +267,57 @@ class SpecialPageTranslationMovePage extends MovePageForm { $base = $this->oldTitle->getPrefixedText(); $target = $this->newTitle; $count = 0; + $subpagesCount = 0; $types = [ 'pt-movepage-list-pages' => [ $this->oldTitle ], 'pt-movepage-list-translation' => $this->getTranslationPages(), 'pt-movepage-list-section' => $this->getSectionPages(), - 'pt-movepage-list-translatable' => $this->getTranslatableSubpages(), - 'pt-movepage-list-other' => $this->getNormalSubpages(), + 'pt-movepage-list-translatable' => $this->getTranslatableSubpages() ]; + if ( TranslateUtils::allowsSubpages( $this->oldTitle ) ) { + $types[ 'pt-movepage-list-other'] = $this->getNormalSubpages(); + } + foreach ( $types as $type => $pages ) { - $out->wrapWikiMsg( '=== $1 ===', [ $type, count( $pages ) ] ); + $pageCount = count( $pages ); + $out->wrapWikiMsg( '=== $1 ===', [ $type, $pageCount ] ); + + if ( !$pageCount ) { + $out->addWikiMsg( 'pt-movepage-list-no-pages' ); + continue; + } + if ( $type === 'pt-movepage-list-translatable' ) { - $out->addWikiMsg( 'pt-movepage-list-translatable-note' ); + $out->wrapWikiMsg( + "'''$1'''", $this->msg( 'pt-movepage-list-translatable-note' ) + ); } $lines = []; foreach ( $pages as $old ) { - $toBeMoved = true; - - // These pages need specific checks - if ( $type === 'pt-movepage-list-other' ) { - $toBeMoved = $this->moveSubpages; - } - - if ( $type === 'pt-movepage-list-translatable' ) { - $toBeMoved = false; + $canBeMoved = $type !== 'pt-movepage-list-translatable'; + if ( $canBeMoved ) { + $count++; } - if ( $toBeMoved ) { - $count++; + if ( $type === 'pt-movepage-list-other' ) { + $subpagesCount++; } - $lines[] = $this->getChangeLine( $base, $old, $target, $toBeMoved ); + $lines[] = $this->getChangeLine( $base, $old, $target, $canBeMoved ); } - TranslateUtils::addWikiTextAsInterface( $out, implode( "\n", $lines ) ); + $out->addWikiTextAsInterface( implode( "\n", $lines ) ); } - TranslateUtils::addWikiTextAsInterface( $out, "----\n" ); - $out->addWikiMsg( 'pt-movepage-list-count', $this->getLanguage()->formatNum( $count ) ); + $out->addWikiTextAsInterface( "----\n" ); + $out->addWikiMsg( + 'pt-movepage-list-count', + $this->getLanguage()->formatNum( $count ), + $this->getLanguage()->formatNum( $subpagesCount ) + ); $br = Html::element( 'br' ); $readonly = [ 'readonly' => 'readonly' ]; @@ -438,57 +421,62 @@ class SpecialPageTranslationMovePage extends MovePageForm { $this->getOutput()->addWikiMsg( 'pt-movepage-started' ); } - protected function checkMoveBlockers() { - $blockers = []; + protected function checkMoveBlockers( User $user ) { + $blockers = new SplObjectStorage(); + $source = $this->oldTitle; $target = $this->newTitle; if ( !$target ) { - $blockers[] = [ 'pt-movepage-block-base-invalid' ]; + $blockers[$source] = Status::newFatal( 'pt-movepage-block-base-invalid' ); return $blockers; } if ( $target->inNamespaces( NS_MEDIAWIKI, NS_TRANSLATIONS ) ) { - $blockers[] = [ 'immobile-target-namespace', $target->getNsText() ]; + $blockers[$source] = Status::newFatal( + 'immobile-target-namespace', $target->getNsText() + ); return $blockers; } - $base = $this->oldTitle->getPrefixedText(); - if ( $target->exists() ) { - $blockers[] = [ 'pt-movepage-block-base-exists', $target->getPrefixedText() ]; + $blockers[$source] = Status::newFatal( + 'pt-movepage-block-base-exists', $target->getPrefixedText() + ); } else { - $errors = $this->oldTitle->isValidMoveOperation( $target, true, $this->reason ); - if ( is_array( $errors ) ) { - $blockers = array_merge( $blockers, $errors ); + $movePage = new MovePage( $this->oldTitle, $target ); + $status = $movePage->isValidMove(); + $status->merge( $movePage->checkPermissions( $user, $this->reason ) ); + if ( !$status->isOK() ) { + $blockers[$source] = $status; } } // Don't spam the same errors for all pages if base page fails - if ( $blockers ) { + if ( count( $blockers ) ) { return $blockers; } // Collect all the old and new titles for checcks $titles = []; - + $base = $this->oldTitle->getPrefixedText(); $pages = $this->getTranslationPages(); foreach ( $pages as $old ) { $titles['tp'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ]; } - $pages = $this->getSectionPages(); - foreach ( $pages as $old ) { - $titles['section'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ]; - } - $subpages = $this->moveSubpages ? $this->getNormalSubpages() : []; foreach ( $subpages as $old ) { $titles['subpage'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ]; } + $pages = $this->getSectionPages(); + foreach ( $pages as $old ) { + $titles['section'][] = [ $old, $this->newPageTitle( $base, $old, $target ) ]; + } + // Check that all new titles are valid $lb = new LinkBatch(); foreach ( $titles as $type => $list ) { @@ -498,10 +486,10 @@ class SpecialPageTranslationMovePage extends MovePageForm { foreach ( $list as $pair ) { list( $old, $new ) = $pair; if ( $new === null ) { - $blockers[] = [ + $blockers[$old] = Status::newFatal( "pt-movepage-block-$type-invalid", $old->getPrefixedText() - ]; + ); continue; } $lb->addObj( $old ); @@ -509,7 +497,7 @@ class SpecialPageTranslationMovePage extends MovePageForm { } } - if ( $blockers ) { + if ( count( $blockers ) ) { return $blockers; } @@ -522,23 +510,27 @@ class SpecialPageTranslationMovePage extends MovePageForm { foreach ( $list as $pair ) { list( $old, $new ) = $pair; if ( $new->exists() ) { - $blockers[] = [ + $blockers[$old] = Status::newFatal( "pt-movepage-block-$type-exists", $old->getPrefixedText(), $new->getPrefixedText() - ]; + ); } else { /* This method has terrible performance: * - 2 queries by core * - 3 queries by lqt * - and no obvious way to preload the data! */ - $errors = $old->isValidMoveOperation( $target, false ); - if ( is_array( $errors ) ) { - $blockers = array_merge( $blockers, $errors ); + $movePage = new MovePage( $old, $target ); + $status = $movePage->isValidMove(); + // Do not check for permissions here, as these pages are not editable/movable + // in regular use + if ( !$status->isOK() ) { + $blockers[$old] = $status; } - /* Because of the above, check only one of the possibly thousands - * of section pages and assume rest are fine. */ + /* Because of the poor performance, check only one of the possibly thousands + * of section pages and assume rest are fine. This assumes section pages are + * listed last in the array. */ if ( $type === 'section' ) { break; } @@ -587,7 +579,7 @@ class SpecialPageTranslationMovePage extends MovePageForm { /** * Returns only translation subpages. - * @return Array of titles. + * @return Title[] */ protected function getTranslationPages() { if ( !isset( $this->translationPages ) ) { @@ -599,15 +591,20 @@ class SpecialPageTranslationMovePage extends MovePageForm { /** * Returns all subpages, if the namespace has them enabled. - * @return mixed TitleArray, or empty array if this page's namespace doesn't allow subpages + * @return Title[] */ protected function getSubpages() { - return $this->page->getTitle()->getSubpages(); + $pages = $this->page->getTitle()->getSubpages(); + if ( $pages instanceof Traversable ) { + $pages = iterator_to_array( $pages ); + } + + return $pages; } private function getNormalSubpages() { return array_filter( - iterator_to_array( $this->getSubpages() ), + $this->getSubpages(), function ( $page ) { return !( TranslatablePage::isTranslationPage( $page ) || @@ -619,7 +616,7 @@ class SpecialPageTranslationMovePage extends MovePageForm { private function getTranslatableSubpages() { return array_filter( - iterator_to_array( $this->getSubpages() ), + $this->getSubpages(), function ( $page ) { return TranslatablePage::isSourcePage( $page ); } diff --git a/MLEB/Translate/tag/TPParse.php b/MLEB/Translate/tag/TPParse.php index 9e1b32bc..07a4813e 100644 --- a/MLEB/Translate/tag/TPParse.php +++ b/MLEB/Translate/tag/TPParse.php @@ -168,7 +168,7 @@ class TPParse { * translation tags removed and outdated translation marked with a class * mw-translate-fuzzy. * - * @param MessageCollection $collection Collection that holds translated messages. + * @param MessageCollection|array $collection Collection that holds translated messages. * @param bool $showOutdated Whether to show outdated sections, wrapped in a HTML class. * @return string Whole page as wikitext. */ @@ -201,12 +201,15 @@ class TPParse { // We do not ever want to show explicit fuzzy marks in the rendered pages $sectiontext = str_replace( TRANSLATE_FUZZY, '', $sectiontext ); - if ( $s->isInline() ) { - $sectiontext = "<span class=\"mw-translate-fuzzy\">$sectiontext</span>"; - } else { - // We add new lines around the text to avoid disturbing any mark-up that - // has special handling on line start, such as lists. - $sectiontext = "<div class=\"mw-translate-fuzzy\">\n$sectiontext\n</div>"; + if ( $s->canWrap() ) { + if ( $s->isInline() ) { + $sectiontext = "<span class=\"mw-translate-fuzzy\">$sectiontext</span>"; + } else { + // We add new lines around the text to avoid disturbing any mark-up that + // has special handling on line start, such as lists. + $sectiontext = + "<div class=\"mw-translate-fuzzy\">\n$sectiontext\n</div>"; + } } } } @@ -230,21 +233,21 @@ class TPParse { $nph = []; $text = TranslatablePage::armourNowiki( $nph, $text ); - // Remove translation markup from the template to produce final text + // Remove translation markup from the template to produce the final text. $cb = [ __CLASS__, 'replaceTagCb' ]; - $text = preg_replace_callback( '~(<translate>)(.*)(</translate>)~sU', $cb, $text ); + $text = preg_replace_callback( '~<translate(?: nowrap)?>(.*?)</translate>~s', $cb, $text ); $text = TranslatablePage::unArmourNowiki( $nph, $text ); return $text; } /** - * Chops of trailing or preceeding whitespace intelligently to avoid - * build up of unintented whitespace. + * Chops of trailing or preceding whitespace intelligently to avoid + * build up of unintended whitespace. * @param string[] $matches * @return string */ protected static function replaceTagCb( $matches ) { - return preg_replace( '~^\n|\n\z~', '', $matches[2] ); + return preg_replace( '~^\n|\n\z~', '', $matches[1] ); } } diff --git a/MLEB/Translate/tag/TPSection.php b/MLEB/Translate/tag/TPSection.php index 49b3f104..4624ded9 100644 --- a/MLEB/Translate/tag/TPSection.php +++ b/MLEB/Translate/tag/TPSection.php @@ -19,9 +19,9 @@ class TPSection { public $id; /** - * @var string New name of the section, that will be saved to database. + * @var string|null New name of the section, that will be saved to database. */ - public $name; + public $name = null; /** * @var string Section text. @@ -34,9 +34,9 @@ class TPSection { public $type; /** - * @var string Text of previous version of this section. + * @var string|null Text of previous version of this section. */ - public $oldText; + public $oldText = null; /** * @var bool Whether this section is inline section. @@ -44,6 +44,9 @@ class TPSection { */ protected $inline = false; + /** @var bool Whether wrapping the section is allowed */ + private $canWrap = true; + /** * @var int Version number for the serialization. */ @@ -63,6 +66,22 @@ class TPSection { } /** + * @param bool $value + * @since 2020.07 + */ + public function setCanWrap( bool $value ): void { + $this->canWrap = $value; + } + + /** + * @return bool + * @since 2020.07 + */ + public function canWrap(): bool { + return $this->canWrap; + } + + /** * Returns section text unmodified. * @return string Wikitext. */ @@ -97,7 +116,7 @@ class TPSection { * @return string Wikitext. */ public function getMarkedText() { - $id = isset( $this->name ) ? $this->name : $this->id; + $id = $this->name ?? $this->id; $header = "<!--T:{$id}-->"; $re = '~^(=+.*?=+\s*?$)~m'; $rep = "\\1 $header"; @@ -121,7 +140,7 @@ class TPSection { * @return string Wikitext. */ public function getOldText() { - return isset( $this->oldText ) ? $this->oldText : $this->text; + return $this->oldText ?? $this->text; } /** diff --git a/MLEB/Translate/tag/TranslatablePage.php b/MLEB/Translate/tag/TranslatablePage.php index 51a32db9..f6a838cb 100644 --- a/MLEB/Translate/tag/TranslatablePage.php +++ b/MLEB/Translate/tag/TranslatablePage.php @@ -7,6 +7,10 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; +use MediaWiki\Revision\RevisionLookup; +use MediaWiki\Revision\SlotRecord; use Wikimedia\Rdbms\Database; /** @@ -21,7 +25,7 @@ class TranslatablePage { protected $title; /** - * Text contents of the page. + * @var ?string Text contents of the page. */ protected $text; @@ -39,11 +43,6 @@ class TranslatablePage { protected $source; /** - * Whether the page contents is already loaded. - */ - protected $init = false; - - /** * Name of the section which contains the translated page title. */ protected $displayTitle = 'Page display title'; @@ -91,8 +90,10 @@ class TranslatablePage { * @throws MWException * @return self */ - public static function newFromRevision( Title $title, $revision ) { - $rev = Revision::newFromTitle( $title, $revision ); + public static function newFromRevision( Title $title, int $revision ) { + $rev = MediaWikiServices::getInstance() + ->getRevisionLookup() + ->getRevisionByTitle( $title, $revision ); if ( $rev === null ) { throw new MWException( 'Revision is null' ); } @@ -106,7 +107,7 @@ class TranslatablePage { /** * Constructs a translatable page from title. - * The text of last marked revision is loaded when neded. + * The text of last marked revision is loaded when needed. * * @param Title $title * @return self @@ -128,29 +129,38 @@ class TranslatablePage { /** * Returns the text for this translatable page. - * @throws MWException * @return string */ - public function getText() { - if ( $this->init === false ) { - switch ( $this->source ) { - case 'text': - break; - /** @noinspection PhpMissingBreakStatementInspection */ - case 'title': - $this->revision = $this->getMarkedTag(); - case 'revision': - $rev = Revision::newFromTitle( $this->getTitle(), $this->revision ); - $this->text = ContentHandler::getContentText( $rev->getContent() ); - break; + public function getText(): string { + if ( $this->text !== null ) { + return $this->text; + } + + $page = $this->getTitle()->getPrefixedDBkey(); + + if ( $this->source === 'title' ) { + $revision = $this->getMarkedTag(); + if ( !is_int( $revision ) ) { + throw new LogicException( + "Trying to load a text for $page which is not marked for translation" + ); } + $this->revision = $revision; } - if ( !is_string( $this->text ) ) { - throw new MWException( 'We have no text' ); + $flags = TranslateUtils::shouldReadFromMaster() + ? RevisionLookup::READ_LATEST + : RevisionLookup::READ_NORMAL; + $rev = MediaWikiServices::getInstance() + ->getRevisionLookup() + ->getRevisionByTitle( $this->getTitle(), $this->revision, $flags ); + $text = ContentHandler::getContentText( $rev->getContent( SlotRecord::MAIN ) ); + + if ( !is_string( $text ) ) { + throw new RuntimeException( "Failed to load text for $page" ); } - $this->init = true; + $this->text = $text; return $this->text; } @@ -159,21 +169,11 @@ class TranslatablePage { * Revision is null if object was constructed using newFromText. * @return null|int */ - public function getRevision() { + public function getRevision(): ?int { return $this->revision; } /** - * Manually set a revision number to use loading page text. - * @param int $revision - */ - public function setRevision( $revision ) { - $this->revision = $revision; - $this->source = 'revision'; - $this->init = false; - } - - /** * Returns the source language of this translatable page. In other words * the language in which the page without language code is written. * @return string @@ -282,41 +282,38 @@ class TranslatablePage { $tagPlaceHolders = []; while ( true ) { - $re = '~(<translate>)(.*?)(</translate>)~s'; + $re = '~(<translate(?: nowrap)?>)(.*?)</translate>~s'; $matches = []; - $ok = preg_match_all( $re, $text, $matches, PREG_OFFSET_CAPTURE ); + $ok = preg_match( $re, $text, $matches, PREG_OFFSET_CAPTURE ); - if ( $ok === 0 ) { - break; // No matches + if ( $ok === 0 || $ok === false ) { + break; // No match or failure } - // Do-placehold for the whole stuff + $contentWithTags = $matches[0][0]; + $contentWithoutTags = $matches[2][0]; + // These are offsets to the content inside the tags in $text + $offsetStart = $matches[0][1]; + $offsetEnd = $offsetStart + strlen( $contentWithTags ); + + // Replace the whole match with a placeholder $ph = TranslateUtils::getPlaceholder(); - $start = $matches[0][0][1]; - $len = strlen( $matches[0][0][0] ); - $end = $start + $len; - $text = self::index_replace( $text, $ph, $start, $end ); - - // Sectionise the contents - // Strip the surrounding tags - $contents = $matches[0][0][0]; // full match - $start = $matches[2][0][1] - $matches[0][0][1]; // bytes before actual content - $len = strlen( $matches[2][0][0] ); // len of the content - $end = $start + $len; - - $sectiontext = substr( $contents, $start, $len ); - - if ( strpos( $sectiontext, '<translate>' ) !== false ) { - throw new TPException( [ 'pt-parse-nested', $sectiontext ] ); + $text = substr( $text, 0, $offsetStart ) . $ph . substr( $text, $offsetEnd ); + + if ( preg_match( '~<translate( nowrap)?>~', $contentWithoutTags ) !== 0 ) { + throw new TPException( [ 'pt-parse-nested', $contentWithoutTags ] ); } - $sectiontext = self::unArmourNowiki( $nowiki, $sectiontext ); + $openTag = $matches[1][0]; + $canWrap = $openTag !== '<translate nowrap>'; - $parse = self::sectionise( $sectiontext ); - $sections += $parse['sections']; + // Parse the content inside the tags + $contentWithoutTags = self::unArmourNowiki( $nowiki, $contentWithoutTags ); + $parse = self::sectionise( $contentWithoutTags, $canWrap ); - $tagPlaceHolders[$ph] = - self::index_replace( $contents, $parse['template'], $start, $end ); + // Update list of sections and the template with the results + $sections += $parse['sections']; + $tagPlaceHolders[$ph] = $openTag . $parse['template'] . '</translate>'; } $prettyTemplate = $text; @@ -324,15 +321,14 @@ class TranslatablePage { $prettyTemplate = str_replace( $ph, '[...]', $prettyTemplate ); } - if ( strpos( $text, '<translate>' ) !== false ) { + if ( preg_match( '~<translate( nowrap)?>~', $text ) !== 0 ) { throw new TPException( [ 'pt-parse-open', $prettyTemplate ] ); } elseif ( strpos( $text, '</translate>' ) !== false ) { throw new TPException( [ 'pt-parse-close', $prettyTemplate ] ); } - foreach ( $tagPlaceHolders as $ph => $value ) { - $text = str_replace( $ph, $value, $text ); - } + // Replace the tag placeholders with unit placeholders to form the template + $text = strtr( $text, $tagPlaceHolders ); if ( count( $sections ) === 1 ) { // Don't return display title for pages which have no sections @@ -362,7 +358,7 @@ class TranslatablePage { public static function cleanupTags( $text ) { $nowiki = []; $text = self::armourNowiki( $nowiki, $text ); - $text = preg_replace( '~<translate>\n?~s', '', $text ); + $text = preg_replace( '~<translate( nowrap)?>\n?~s', '', $text ); $text = preg_replace( '~\n?</translate>~s', '', $text ); // Mirroring what TPSection::getTextForTrans does $text = preg_replace( '~<tvar\|([^>]+)>(.*?)</>~u', '\2', $text ); @@ -402,25 +398,15 @@ class TranslatablePage { } /** - * @param string $string - * @param string $rep - * @param int $start - * @param int $end - * @return string - */ - protected static function index_replace( $string, $rep, $start, $end ) { - return substr( $string, 0, $start ) . $rep . substr( $string, $end ); - } - - /** * Splits the content marked with \<translate> tags into sections, which * are separated with with two or more newlines. Extra whitespace is captured * in the template and is not included in the sections. * * @param string $text Contents of one pair of \<translate> tags. + * @param bool $canWrap * @return array Contains a template and array of unparsed sections. */ - public static function sectionise( $text ) { + public static function sectionise( string $text, bool $canWrap ): array { $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE; $parts = preg_split( '~(^\s*|\s*\n\n\s*|\s*$)~', $text, -1, $flags ); @@ -436,6 +422,7 @@ class TranslatablePage { $ph = TranslateUtils::getPlaceholder(); $tpsection = self::shakeSection( $_ ); $tpsection->setIsInline( $inline ); + $tpsection->setCanWrap( $canWrap ); $sections[$ph] = $tpsection; $template .= $ph; } @@ -507,6 +494,7 @@ class TranslatablePage { */ public function addMarkedTag( $revision, $value = null ) { $this->addTag( 'tp:mark', $revision, $value ); + self::clearSourcePageCache(); } /** @@ -584,6 +572,7 @@ class TranslatablePage { $dbw->delete( 'revtag', $conds, __METHOD__ ); $dbw->delete( 'translate_sections', [ 'trs_page' => $aid ], __METHOD__ ); unset( self::$tagCache[$aid] ); + self::clearSourcePageCache(); } /** @@ -689,7 +678,7 @@ class TranslatablePage { * @return string[] List of string * @since 2012-08-06 */ - protected function getSections() { + public function getSections() { $dbr = TranslateUtils::getSafeReadDB(); $conds = [ 'trs_page' => $this->getTitle()->getArticleID() ]; @@ -707,7 +696,7 @@ class TranslatablePage { * Returns a list of translation unit pages. * @param string $set Can be either 'all', or 'active' * @param string|bool $code Only list unit pages in given language. - * @return Title[] List of Titles. + * @return Title[] * @since 2012-08-06 */ public function getTranslationUnitPages( $set = 'active', $code = false ) { @@ -851,29 +840,65 @@ class TranslatablePage { } /** + * Helper to guess translation page from translation unit. + * + * @param LinkTarget $translationUnit + * @return array + * @since 2019.10 + */ + public static function parseTranslationUnit( LinkTarget $translationUnit ) : array { + // Format is Translations:SourcePageNamespace:SourcePageName/SectionName/LanguageCode. + // We will drop the namespace immediately here. + $parts = explode( '/', $translationUnit->getText() ); + + // LanguageCode and SectionName are guaranteed to not have '/'. + $language = array_pop( $parts ); + $section = array_pop( $parts ); + $sourcepage = implode( '/', $parts ); + + return [ + 'sourcepage' => $sourcepage, + 'section' => $section, + 'language' => $language + ]; + } + + /** * @param Title $title * @return bool */ public static function isSourcePage( Title $title ) { - $cache = ObjectCache::getMainWANInstance(); - $pcTTL = $cache::TTL_PROC_LONG; + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $cacheKey = $cache->makeKey( 'pagetranslation', 'sourcepages' ); $translatablePageIds = $cache->getWithSetCallback( - $cache->makeKey( 'pagetranslation', 'sourcepages' ), - $cache::TTL_MINUTE * 5, + $cacheKey, + $cache::TTL_HOUR * 2, function ( $oldValue, &$ttl, array &$setOpts ) { $dbr = wfGetDB( DB_REPLICA ); $setOpts += Database::getCacheSetOptions( $dbr ); return self::getTranslatablePages(); }, - [ 'pcTTL' => $pcTTL, 'pcGroup' => __CLASS__ . ':30' ] + [ + 'checkKeys' => [ $cacheKey ], + 'pcTTL' => $cache::TTL_PROC_SHORT, + 'pcGroup' => __CLASS__ . ':1' + ] ); return in_array( $title->getArticleID(), $translatablePageIds ); } /** + * Clears the source page cache + */ + public static function clearSourcePageCache(): void { + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); + $cache->touchCheckKey( $cache->makeKey( 'pagetranslation', 'sourcepages' ) ); + } + + /** * Get a list of page ids where the latest revision is either tagged or marked * @return array */ diff --git a/MLEB/Translate/tag/TranslatablePageMoveJob.php b/MLEB/Translate/tag/TranslatablePageMoveJob.php index 47736e82..c0b1e189 100644 --- a/MLEB/Translate/tag/TranslatablePageMoveJob.php +++ b/MLEB/Translate/tag/TranslatablePageMoveJob.php @@ -7,6 +7,8 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot; + /** * Contains class with job for moving translation pages. Used together with * SpecialPageTranslationMovePage class. @@ -43,7 +45,6 @@ class TranslatablePageMoveJob extends Job { public function __construct( $title, $params = [] ) { parent::__construct( __CLASS__, $title, $params ); - $this->params = $params; } public function run() { @@ -117,6 +118,7 @@ class TranslatablePageMoveJob extends Job { protected function moveMetadata( $oldGroupId, $newGroupId ) { $types = [ 'prioritylangs', 'priorityforce', 'priorityreason' ]; + TranslateMetadata::preloadGroups( [ $oldGroupId, $newGroupId ] ); foreach ( $types as $type ) { $value = TranslateMetadata::get( $oldGroupId, $type ); if ( $value !== false ) { @@ -126,13 +128,11 @@ class TranslatablePageMoveJob extends Job { } // Make the changes in aggregate groups metadata, if present in any of them. - $groups = MessageGroups::getAllGroups(); - foreach ( $groups as $group ) { - if ( !$group instanceof AggregateMessageGroup ) { - continue; - } + $aggregateGroups = MessageGroups::getGroupsByType( AggregateMessageGroup::class ); + TranslateMetadata::preloadGroups( array_keys( $aggregateGroups ) ); - $subgroups = TranslateMetadata::get( $group->getId(), 'subgroups' ); + foreach ( $aggregateGroups as $id => $group ) { + $subgroups = TranslateMetadata::get( $id, 'subgroups' ); if ( $subgroups === false ) { continue; } diff --git a/MLEB/Translate/tag/TranslateDeleteJob.php b/MLEB/Translate/tag/TranslateDeleteJob.php index 38d32f89..a25e38d4 100644 --- a/MLEB/Translate/tag/TranslateDeleteJob.php +++ b/MLEB/Translate/tag/TranslateDeleteJob.php @@ -8,6 +8,8 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot; + /** * Contains class with job for deleting translatable and translation pages. * @@ -22,7 +24,7 @@ class TranslateDeleteJob extends Job { * @param string $reason * @return self */ - public static function newJob( Title $target, $base, $full, /*User*/$performer, $reason ) { + public static function newJob( Title $target, $base, $full, /*User*/ $performer, $reason ) { $job = new self( $target ); $job->setUser( FuzzyBot::getUser() ); $job->setFull( $full ); @@ -58,8 +60,32 @@ class TranslateDeleteJob extends Job { $error = ''; $wikipage = new WikiPage( $title ); - $status = $wikipage->doDeleteArticleReal( "{$summary}: $reason", false, 0, true, $error, - $user, [], 'delete', true ); + if ( version_compare( MW_VERSION, '1.35', '<' ) ) { + $status = $wikipage->doDeleteArticleReal( + "{$summary}: $reason", + false, + 0, + true, + $error, + $user, + [], + 'delete', + true + ); + } else { + $status = $wikipage->doDeleteArticleReal( + "{$summary}: $reason", + $user, + false, + null, + $error, + null, + [], + 'delete', + true + ); + } + if ( !$status->isGood() ) { $params = [ 'target' => $base, @@ -93,7 +119,7 @@ class TranslateDeleteJob extends Job { $entry->publish( $logid ); $tpage = TranslatablePage::newFromTitle( $title ); - $tpage->getTranslationPercentages( true ); + $tpage->getTranslationPercentages(); foreach ( $tpage->getTranslationPages() as $page ) { $page->invalidateCache(); } diff --git a/MLEB/Translate/tag/TranslateMoveJob.php b/MLEB/Translate/tag/TranslateMoveJob.php deleted file mode 100644 index 8a06258b..00000000 --- a/MLEB/Translate/tag/TranslateMoveJob.php +++ /dev/null @@ -1,221 +0,0 @@ -<?php -/** - * Contains class with job for moving translation pages. - * - * @file - * @author Niklas Laxström - * @copyright Copyright © 2008-2010, Niklas Laxström - * @license GPL-2.0-or-later - */ - -/** - * Contains class with job for moving translation pages. Used together with - * PageTranslationMovePage class. - * - * @ingroup PageTranslation JobQueue - */ -class TranslateMoveJob extends Job { - /** - * @param Title $source - * @param Title $target - * @param array $params should include base-source and base-target - * @param User $performer - * @return self - */ - public static function newJob( Title $source, Title $target, array $params, - /*User*/$performer - ) { - $job = new self( $source ); - $job->setUser( FuzzyBot::getUser() ); - $job->setTarget( $target->getPrefixedText() ); - $summary = wfMessage( 'pt-movepage-logreason', $params['base-source'] ); - $summary = $summary->inContentLanguage()->text(); - $job->setSummary( $summary ); - $job->setParams( $params ); - $job->setPerformer( $performer ); - $job->lock(); - - return $job; - } - - /** - * @param Title $title - * @param array $params - */ - public function __construct( $title, $params = [] ) { - parent::__construct( __CLASS__, $title, $params ); - } - - public function run() { - // Unfortunately the global is needed until bug is fixed: - // https://phabricator.wikimedia.org/T51086 - // Once MW >= 1.24 is supported, can use MovePage class. - global $wgUser; - - // Initialization - $title = $this->title; - // Other stuff - $user = $this->getUser(); - $summary = $this->getSummary(); - $target = $this->getTarget(); - $base = $this->params['base-source']; - $doer = User::newFromName( $this->getPerformer() ); - - PageTranslationHooks::$allowTargetEdit = true; - PageTranslationHooks::$jobQueueRunning = true; - $oldUser = $wgUser; - $wgUser = $user; - self::forceRedirects( false ); - - // Don't check perms, don't leave a redirect - $ok = $title->moveTo( $target, false, $summary, false ); - if ( !$ok ) { - $params = [ - 'target' => $target->getPrefixedText(), - 'error' => $ok, - ]; - - $entry = new ManualLogEntry( 'pagetranslation', 'movenok' ); - $entry->setPerformer( $doer ); - $entry->setTarget( $title ); - $entry->setParameters( $params ); - $logid = $entry->insert(); - $entry->publish( $logid ); - } - - self::forceRedirects( true ); - PageTranslationHooks::$allowTargetEdit = false; - - $this->unlock(); - - $cache = wfGetCache( CACHE_ANYTHING ); - $key = wfMemcKey( 'translate-pt-move', $base ); - - $count = $cache->decr( $key ); - $last = (string)$count === '0'; - - if ( $last ) { - $cache->delete( $key ); - - $params = [ - 'target' => $this->params['base-target'], - ]; - - $entry = new ManualLogEntry( 'pagetranslation', 'moveok' ); - $entry->setPerformer( $doer ); - $entry->setParameters( $params ); - $entry->setTarget( Title::newFromText( $base ) ); - $logid = $entry->insert(); - $entry->publish( $logid ); - - PageTranslationHooks::$jobQueueRunning = false; - } - - $wgUser = $oldUser; - - return true; - } - - public function setSummary( $summary ) { - $this->params['summary'] = $summary; - } - - public function getSummary() { - return $this->params['summary']; - } - - public function setPerformer( $performer ) { - if ( is_object( $performer ) ) { - $this->params['performer'] = $performer->getName(); - } else { - $this->params['performer'] = $performer; - } - } - - public function getPerformer() { - return $this->params['performer']; - } - - /** - * @param Title|string $target - */ - public function setTarget( $target ) { - if ( $target instanceof Title ) { - $this->params['target'] = $target->getPrefixedText(); - } else { - $this->params['target'] = $target; - } - } - - public function getTarget() { - return Title::newFromText( $this->params['target'] ); - } - - public function setUser( $user ) { - if ( is_object( $user ) ) { - $this->params['user'] = $user->getName(); - } else { - $this->params['user'] = $user; - } - } - - /** - * Get a user object for doing edits. - * @return User - */ - public function getUser() { - return User::newFromName( $this->params['user'], false ); - } - - public function setParams( array $params ) { - foreach ( $params as $k => $v ) { - $this->params[$k] = $v; - } - } - - public function lock() { - $cache = wfGetCache( CACHE_ANYTHING ); - $cache->set( wfMemcKey( 'pt-lock', sha1( $this->title->getPrefixedText() ) ), true ); - $cache->set( wfMemcKey( 'pt-lock', sha1( $this->getTarget()->getPrefixedText() ) ), true ); - } - - public function unlock() { - $cache = wfGetCache( CACHE_ANYTHING ); - $cache->delete( wfMemcKey( 'pt-lock', sha1( $this->title->getPrefixedText() ) ) ); - $cache->delete( wfMemcKey( 'pt-lock', sha1( $this->getTarget()->getPrefixedText() ) ) ); - } - - /** - * Adapted from wfSuppressWarnings to allow not leaving redirects. - * @param bool $end - */ - public static function forceRedirects( $end = false ) { - static $suppressCount = 0; - static $originalLevel = null; - - global $wgGroupPermissions; - global $wgUser; - - if ( $end ) { - if ( $suppressCount ) { - --$suppressCount; - if ( !$suppressCount ) { - if ( $originalLevel === null ) { - unset( $wgGroupPermissions['*']['suppressredirect'] ); - } else { - $wgGroupPermissions['*']['suppressredirect'] = $originalLevel; - } - } - } - } else { - if ( !$suppressCount ) { - $originalLevel = isset( $wgGroupPermissions['*']['suppressredirect'] ) ? - $wgGroupPermissions['*']['suppressredirect'] : - null; - $wgGroupPermissions['*']['suppressredirect'] = true; - } - ++$suppressCount; - } - $wgUser->clearInstanceCache(); - } -} diff --git a/MLEB/Translate/tag/TranslateRenderJob.php b/MLEB/Translate/tag/TranslateRenderJob.php index a8ccf3c0..27fe1366 100644 --- a/MLEB/Translate/tag/TranslateRenderJob.php +++ b/MLEB/Translate/tag/TranslateRenderJob.php @@ -7,12 +7,15 @@ * @license GPL-2.0-or-later */ +use MediaWiki\Extensions\Translate\Jobs\GenericTranslateJob; +use MediaWiki\Extensions\Translate\SystemUsers\FuzzyBot; + /** * Job for updating translation pages when translation or template changes. * * @ingroup PageTranslation JobQueue */ -class TranslateRenderJob extends Job { +class TranslateRenderJob extends GenericTranslateJob { /** * @param Title $target @@ -33,21 +36,23 @@ class TranslateRenderJob extends Job { */ public function __construct( $title, $params = [] ) { parent::__construct( __CLASS__, $title, $params ); - $this->params = $params; $this->removeDuplicates = true; } public function run() { global $wgTranslateKeepOutdatedTranslations; + $this->logInfo( 'Starting TranslateRenderJob' ); + // Initialization $title = $this->title; - list( , $code ) = TranslateUtils::figureMessage( $title->getPrefixedText() ); + [ , $code ] = TranslateUtils::figureMessage( $title->getPrefixedText() ); // Return the actual translation page... $page = TranslatablePage::isTranslationPage( $title ); if ( !$page ) { - throw new MWException( "Cannot render translation page for {$title->getPrefixedText()}!" ); + $this->logError( 'Cannot render translation page!' ); + return false; } $group = $page->getMessageGroup(); @@ -68,10 +73,21 @@ class TranslateRenderJob extends Job { // @todo FuzzyBot hack PageTranslationHooks::$allowTargetEdit = true; $content = ContentHandler::makeContent( $text, $page->getTitle() ); - $page->doEditContent( $content, $summary, $flags, false, $user ); + $editStatus = $page->doEditContent( $content, $summary, $flags, false, $user ); + if ( !$editStatus->isOK() ) { + $this->logError( + 'Error while editing content in page.', + [ + 'content' => $content, + 'errors' => $editStatus->getErrors() + ] + ); + } + $this->logInfo( 'Finished page edit operation' ); PageTranslationHooks::$allowTargetEdit = false; + $this->logInfo( 'Finished TranslateRenderJob' ); return true; } diff --git a/MLEB/Translate/tag/TranslationsUpdateJob.php b/MLEB/Translate/tag/TranslationsUpdateJob.php index f3020f41..0384b4ed 100644 --- a/MLEB/Translate/tag/TranslationsUpdateJob.php +++ b/MLEB/Translate/tag/TranslationsUpdateJob.php @@ -1,4 +1,6 @@ <?php +use MediaWiki\Extensions\Translate\Jobs\GenericTranslateJob; + /** * Job for updating translation units and translation pages when * a translatable page is marked for translation. @@ -9,7 +11,7 @@ * * @since 2016.03 */ -class TranslationsUpdateJob extends Job { +class TranslationsUpdateJob extends GenericTranslateJob { /** * @inheritDoc */ @@ -39,6 +41,12 @@ class TranslationsUpdateJob extends Job { } public function run() { + // WARNING: Nothing here must not depend on message index being up to date. + // For performance reasons, message index rebuild is run a separate job after + // everything else is updated. + + $this->logInfo( 'Starting TranslationsUpdateJob' ); + $page = TranslatablePage::newFromTitle( $this->title ); $sections = $this->params[ 'sections' ]; foreach ( $sections as $index => $section ) { @@ -56,25 +64,46 @@ class TranslationsUpdateJob extends Job { $job->run(); } + $this->logInfo( + 'Finished running ' . count( $unitJobs ) . ' MessageUpdate jobs for ' + . count( $sections ) . ' sections' + ); + // Ensure we are using the latest group definitions. This is needed so // that in long running scripts we do see the page which was just // marked for translation. Otherwise getMessageGroup in the next line // returns null. There is no need to regenerate the global cache. MessageGroups::singleton()->clearProcessCache(); - // Ensure fresh definitions for MessageIndex and stats + // Ensure fresh definitions for stats $page->getMessageGroup()->clearCaches(); - MessageIndex::singleton()->rebuild(); + $this->logInfo( 'Cleared caches' ); - // Refresh translations statistics + // Refresh translations statistics, we want these to be up to date for the + // RenderJobs, for displaying up to date statistics on the translation pages. $id = $page->getMessageGroupId(); MessageGroupStats::forGroup( $id, MessageGroupStats::FLAG_NO_CACHE ); + $this->logInfo( 'Updated the message group stats' ); + // Try to avoid stale statistics on the base page $wikiPage = WikiPage::factory( $page->getTitle() ); $wikiPage->doPurge(); + $this->logInfo( 'Finished purging' ); + // These can be run independently and in parallel if possible $renderJobs = self::getRenderJobs( $page ); JobQueueGroup::singleton()->push( $renderJobs ); + $this->logInfo( 'Added ' . count( $renderJobs ) . ' RenderJobs to the queue' ); + + // Schedule message index update. Thanks to front caching, it is okay if this takes + // a while (and on large wikis it does take a while!). Running it as a separate job + // also allows de-duplication in case multiple translatable pages are being marked + // for translation in a short period of time. + $job = MessageIndexRebuildJob::newJob(); + JobQueueGroup::singleton()->push( $job ); + + $this->logInfo( 'Finished TranslationsUpdateJob' ); + return true; } |