src/PageTranslation/RenderTranslationPageJob.php
<?php
declare( strict_types = 1 );
namespace MediaWiki\Extension\Translate\PageTranslation;
use JobQueueGroup;
use MediaWiki\CommentStore\CommentStoreComment;
use MediaWiki\Extension\Translate\Jobs\GenericTranslateJob;
use MediaWiki\Extension\Translate\MessageGroupProcessing\DeleteTranslatableBundleJob;
use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserRigorOptions;
use RecentChange;
/**
* Job for updating translation pages when translation or template changes.
* @author Niklas Laxström
* @license GPL-2.0-or-later
* @ingroup PageTranslation JobQueue
*/
class RenderTranslationPageJob extends GenericTranslateJob {
public const ACTION_DELETE = 'delete';
public static function newJob(
Title $target,
?string $triggerAction = null,
?string $unitTitleText = null
): self {
$job = new self( $target, [ 'triggerAction' => $triggerAction, 'unitTitle' => $unitTitleText ] );
$job->setUser( FuzzyBot::getUser() );
$job->setFlags( EDIT_FORCE_BOT );
$job->setSummary( wfMessage( 'tpt-render-summary' )->inContentLanguage()->text() );
return $job;
}
public static function newNonPrioritizedJob(
Title $target,
?string $triggerAction = null,
?string $unitTitleText = null
): self {
$job = self::newJob( $target, $triggerAction, $unitTitleText );
$job->command = 'NonPrioritizedRenderTranslationPageJob';
return $job;
}
public function __construct( Title $title, array $params = [] ) {
parent::__construct( 'RenderTranslationPageJob', $title, $params );
$this->removeDuplicates = true;
}
public function run(): bool {
$this->logJobStart();
$mwServices = MediaWikiServices::getInstance();
// We may be doing double wait here if this job was spawned by TranslationUpdateJob
$lb = $mwServices->getDBLoadBalancerFactory();
if ( !$lb->waitForReplication() ) {
$this->logWarning( 'Continuing despite replication lag' );
}
// Initialization
$translationPageTitle = $this->title;
$tpPage = TranslatablePage::getTranslationPageFromTitle( $translationPageTitle );
if ( !$tpPage ) {
$this->logError( 'Cannot render translation page!' );
return false;
}
// Other stuff
$user = $this->getUser();
$summary = $this->getSummary();
$flags = $this->getFlags();
// We should not re-create the translation page if a translation unit is being deleted
// because it is possible that the translation page may also be queued for deletion.
// Hence, set the flag to EDIT_UPDATE and remove EDIT_NEW if its added
if ( $this->isDeleteTrigger() ) {
$flags = ( $flags | EDIT_UPDATE ) & ~EDIT_NEW;
}
// @todo FuzzyBot hack
Hooks::$allowTargetEdit = true;
$commentStoreComment = CommentStoreComment::newUnsavedComment( $summary );
// $percentageTranslated is modified by reference
$content = $tpPage->getPageContent( $mwServices->getParser(), $percentageTranslated );
$translationPageTitleExists = $translationPageTitle->exists();
if ( $percentageTranslated === 0 && !$translationPageTitleExists ) {
Hooks::$allowTargetEdit = false;
$this->logInfo( 'No translations found and translation page does not exist. Nothing to do.' );
return true;
}
if (
$percentageTranslated === 0 &&
$translationPageTitleExists &&
$this->hasOnlyFuzzyBotAsAuthor( $mwServices->getRevisionStore(), $translationPageTitle )
) {
$this->logInfo( 'Deleting translation page having no translations and modified only by Fuzzybot' );
// Page is not translated at all but the translation page exists and has been only edited by FuzzyBot
$this->deleteTranslationPage( $mwServices->getJobQueueGroup(), $translationPageTitle, FuzzyBot::getUser() );
} else {
$pageUpdater = $mwServices->getWikiPageFactory()
->newFromTitle( $translationPageTitle )
->newPageUpdater( $user );
$pageUpdater->setContent( SlotRecord::MAIN, $content );
if ( $user->authorizeWrite( 'autopatrol', $translationPageTitle ) ) {
$pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
}
$pageUpdater->addTag( 'translate-translation-pages' );
$pageUpdater->saveRevision( $commentStoreComment, $flags );
$status = $pageUpdater->getStatus();
if ( !$status->isOK() ) {
if ( $this->isDeleteTrigger() && $status->hasMessage( 'edit-gone-missing' ) ) {
$this->logInfo( 'Translation page missing with delete trigger' );
} else {
$this->logError(
'Error while editing content in page.',
[
'content' => $content->getTextForSummary(),
'errors' => $status->getErrors()
]
);
}
}
}
Hooks::$allowTargetEdit = false;
$this->logInfo( 'Finished TranslateRenderJob' );
return true;
}
public function setFlags( int $flags ): void {
$this->params['flags'] = $flags;
}
private function getFlags(): int {
return $this->params['flags'];
}
public function setSummary( string $summary ): void {
$this->params['summary'] = $summary;
}
/** @inheritDoc */
public function getDeduplicationInfo(): array {
$info = parent::getDeduplicationInfo();
// Unit title is only passed for logging and should not be used for de-duplication
unset( $info['params']['unitTitle'] );
return $info;
}
private function getSummary(): string {
return $this->params['summary'];
}
/** @param UserIdentity|string $user */
public function setUser( $user ): void {
if ( $user instanceof UserIdentity ) {
$this->params['user'] = $user->getName();
} else {
$this->params['user'] = $user;
}
}
/** Get a user object for doing edits. */
private function getUser(): User {
$userFactory = MediaWikiServices::getInstance()->getUserFactory();
return $userFactory->newFromName( $this->params['user'], UserRigorOptions::RIGOR_NONE );
}
private function isDeleteTrigger(): bool {
$triggerAction = $this->params['triggerAction'] ?? null;
return $triggerAction === self::ACTION_DELETE;
}
private function logJobStart(): void {
$unitTitleText = $this->params['unitTitle'] ?? null;
$logMessage = 'Starting TranslateRenderJob ';
if ( $unitTitleText ) {
$logMessage .= "trigged by $unitTitleText ";
}
if ( $this->isDeleteTrigger() ) {
$logMessage .= '- [deletion] ';
}
$this->logInfo( trim( $logMessage ) );
}
private function deleteTranslationPage(
JobQueueGroup $jobQueueGroup,
Title $translationPageTitle,
UserIdentity $performer
): void {
$translatablePageTitle = ( new MessageHandle( $translationPageTitle ) )->getTitleForBase();
$isTranslationPage = true;
$job = DeleteTranslatableBundleJob::newJob(
$translationPageTitle,
$translatablePageTitle->getPrefixedText(),
TranslatablePage::class,
$isTranslationPage,
$performer,
wfMessage( 'pt-deletepage-lang-outdated-logreason' )->inContentLanguage()->text()
);
$jobQueueGroup->push( $job );
}
private function hasOnlyFuzzyBotAsAuthor( RevisionStore $revisionStore, Title $title ): bool {
$fuzzyBot = FuzzyBot::getUser();
$pageAuthors = $revisionStore->getAuthorsBetween( $title->getId() );
foreach ( $pageAuthors as $author ) {
if ( !$author->equals( $fuzzyBot ) ) {
return false;
}
}
return true;
}
}