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