Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenderTranslationPageJob
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 15
1056
0.00% covered (danger)
0.00%
0 / 1
 newJob
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 newNonPrioritizedJob
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
182
 setFlags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFlags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSummary
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeduplicationInfo
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getSummary
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getUser
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isDeleteTrigger
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 logJobStart
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 deleteTranslationPage
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 hasOnlyFuzzyBotAsAuthor
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use JobQueueGroup;
7use MediaWiki\CommentStore\CommentStoreComment;
8use MediaWiki\Extension\Translate\Jobs\GenericTranslateJob;
9use MediaWiki\Extension\Translate\MessageGroupProcessing\DeleteTranslatableBundleJob;
10use MediaWiki\Extension\Translate\MessageLoading\MessageHandle;
11use MediaWiki\Extension\Translate\SystemUsers\FuzzyBot;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Revision\RevisionStore;
14use MediaWiki\Revision\SlotRecord;
15use MediaWiki\Title\Title;
16use MediaWiki\User\UserIdentity;
17use MediaWiki\User\UserRigorOptions;
18use RecentChange;
19use User;
20
21/**
22 * Job for updating translation pages when translation or template changes.
23 * @author Niklas Laxström
24 * @license GPL-2.0-or-later
25 * @ingroup PageTranslation JobQueue
26 */
27class RenderTranslationPageJob extends GenericTranslateJob {
28    public const ACTION_DELETE = 'delete';
29
30    public static function newJob(
31        Title $target,
32        ?string $triggerAction = null,
33        ?string $unitTitleText = null
34    ): self {
35        $job = new self( $target, [ 'triggerAction' => $triggerAction, 'unitTitle' => $unitTitleText ] );
36        $job->setUser( FuzzyBot::getUser() );
37        $job->setFlags( EDIT_FORCE_BOT );
38        $job->setSummary( wfMessage( 'tpt-render-summary' )->inContentLanguage()->text() );
39
40        return $job;
41    }
42
43    public static function newNonPrioritizedJob(
44        Title $target,
45        ?string $triggerAction = null,
46        ?string $unitTitleText = null
47    ): self {
48        $job = self::newJob( $target, $triggerAction, $unitTitleText );
49        $job->command = 'NonPrioritizedRenderTranslationPageJob';
50        return $job;
51    }
52
53    public function __construct( Title $title, array $params = [] ) {
54        parent::__construct( 'RenderTranslationPageJob', $title, $params );
55        $this->removeDuplicates = true;
56    }
57
58    public function run(): bool {
59        $this->logJobStart();
60        $mwServices = MediaWikiServices::getInstance();
61        // We may be doing double wait here if this job was spawned by TranslationUpdateJob
62        $lb = $mwServices->getDBLoadBalancerFactory();
63        if ( !$lb->waitForReplication() ) {
64            $this->logWarning( 'Continuing despite replication lag' );
65        }
66
67        // Initialization
68        $translationPageTitle = $this->title;
69
70        $tpPage = TranslatablePage::getTranslationPageFromTitle( $translationPageTitle );
71        if ( !$tpPage ) {
72            $this->logError( 'Cannot render translation page!' );
73            return false;
74        }
75
76        // Other stuff
77        $user = $this->getUser();
78        $summary = $this->getSummary();
79        $flags = $this->getFlags();
80
81        // We should not re-create the translation page if a translation unit is being deleted
82        // because it is possible that the translation page may also be queued for deletion.
83        // Hence, set the flag to EDIT_UPDATE and remove EDIT_NEW if its added
84        if ( $this->isDeleteTrigger() ) {
85            $flags = ( $flags | EDIT_UPDATE ) & ~EDIT_NEW;
86        }
87
88        // @todo FuzzyBot hack
89        Hooks::$allowTargetEdit = true;
90
91        $commentStoreComment = CommentStoreComment::newUnsavedComment( $summary );
92        // $percentageTranslated is modified by reference
93        $content = $tpPage->getPageContent( $mwServices->getParser(), $percentageTranslated );
94        $translationPageTitleExists = $translationPageTitle->exists();
95        if ( $percentageTranslated === 0 && !$translationPageTitleExists ) {
96            Hooks::$allowTargetEdit = false;
97            $this->logInfo( 'No translations found and translation page does not exist. Nothing to do.' );
98            return true;
99        }
100
101        if (
102            $percentageTranslated === 0 &&
103            $translationPageTitleExists &&
104            $this->hasOnlyFuzzyBotAsAuthor( $mwServices->getRevisionStore(), $translationPageTitle )
105        ) {
106            $this->logInfo( 'Deleting translation page having no translations and modified only by Fuzzybot' );
107            // Page is not translated at all but the translation page exists and has been only edited by FuzzyBot
108            $this->deleteTranslationPage( $mwServices->getJobQueueGroup(), $translationPageTitle, FuzzyBot::getUser() );
109        } else {
110            $pageUpdater = $mwServices->getWikiPageFactory()
111                ->newFromTitle( $translationPageTitle )
112                ->newPageUpdater( $user );
113            $pageUpdater->setContent( SlotRecord::MAIN, $content );
114
115            if ( $user->authorizeWrite( 'autopatrol', $translationPageTitle ) ) {
116                $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
117            }
118
119            $pageUpdater->addTag( 'translate-translation-pages' );
120            $pageUpdater->saveRevision( $commentStoreComment, $flags );
121            $status = $pageUpdater->getStatus();
122
123            if ( !$status->isOK() ) {
124                if ( $this->isDeleteTrigger() && $status->hasMessage( 'edit-gone-missing' ) ) {
125                    $this->logInfo( 'Translation page missing with delete trigger' );
126                } else {
127                    $this->logError(
128                        'Error while editing content in page.',
129                        [
130                            'content' => $content->getTextForSummary(),
131                            'errors' => $status->getErrors()
132                        ]
133                    );
134                }
135            }
136
137            $this->logInfo( 'Finished page edit operation' );
138        }
139
140        Hooks::$allowTargetEdit = false;
141
142        $this->logInfo( 'Finished TranslateRenderJob' );
143        return true;
144    }
145
146    public function setFlags( int $flags ): void {
147        $this->params['flags'] = $flags;
148    }
149
150    private function getFlags(): int {
151        return $this->params['flags'];
152    }
153
154    public function setSummary( string $summary ): void {
155        $this->params['summary'] = $summary;
156    }
157
158    /** @inheritDoc */
159    public function getDeduplicationInfo(): array {
160        $info = parent::getDeduplicationInfo();
161        // Unit title is only passed for logging and should not be used for de-duplication
162        unset( $info['params']['unitTitle'] );
163        return $info;
164    }
165
166    private function getSummary(): string {
167        return $this->params['summary'];
168    }
169
170    /** @param UserIdentity|string $user */
171    public function setUser( $user ): void {
172        if ( $user instanceof UserIdentity ) {
173            $this->params['user'] = $user->getName();
174        } else {
175            $this->params['user'] = $user;
176        }
177    }
178
179    /** Get a user object for doing edits. */
180    private function getUser(): User {
181        $userFactory = MediaWikiServices::getInstance()->getUserFactory();
182        return $userFactory->newFromName( $this->params['user'], UserRigorOptions::RIGOR_NONE );
183    }
184
185    private function isDeleteTrigger(): bool {
186        $triggerAction = $this->params['triggerAction'] ?? null;
187        return $triggerAction === self::ACTION_DELETE;
188    }
189
190    private function logJobStart(): void {
191        $unitTitleText = $this->params['unitTitle'] ?? null;
192        $logMessage = 'Starting TranslateRenderJob ';
193        if ( $unitTitleText ) {
194            $logMessage .= "trigged by $unitTitleText ";
195        }
196
197        if ( $this->isDeleteTrigger() ) {
198            $logMessage .= '- [deletion] ';
199        }
200
201        $this->logInfo( trim( $logMessage ) );
202    }
203
204    private function deleteTranslationPage(
205        JobQueueGroup $jobQueueGroup,
206        Title $translationPageTitle,
207        UserIdentity $performer
208    ): void {
209        $translatablePageTitle = ( new MessageHandle( $translationPageTitle ) )->getTitleForBase();
210        $isTranslationPage = true;
211
212        $job = DeleteTranslatableBundleJob::newJob(
213            $translationPageTitle,
214            $translatablePageTitle->getPrefixedText(),
215            TranslatablePage::class,
216            $isTranslationPage,
217            $performer,
218            wfMessage( 'pt-deletepage-lang-outdated-logreason' )->inContentLanguage()->text()
219        );
220
221        $jobQueueGroup->push( $job );
222    }
223
224    private function hasOnlyFuzzyBotAsAuthor( RevisionStore $revisionStore, Title $title ): bool {
225        $fuzzyBot = FuzzyBot::getUser();
226        $pageAuthors = $revisionStore->getAuthorsBetween( $title->getId() );
227        foreach ( $pageAuthors as $author ) {
228            if ( !$author->equals( $fuzzyBot ) ) {
229                return false;
230            }
231        }
232        return true;
233    }
234}