Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TranslatableBundleImporter
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 7
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getInstance
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 import
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
42
 setPageImportCompleteCallback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logImport
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 addReadyTagForTranslatablePage
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 onAfterImportPage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageGroupProcessing;
5
6use Closure;
7use Exception;
8use ImportStreamSource;
9use ManualLogEntry;
10use MediaWiki\Extension\Translate\PageTranslation\TranslatablePage;
11use MediaWiki\Extension\Translate\PageTranslation\TranslatablePageParser;
12use MediaWiki\Extension\Translate\Services;
13use MediaWiki\Hook\AfterImportPageHook;
14use MediaWiki\Permissions\UltimateAuthority;
15use MediaWiki\Revision\RevisionLookup;
16use MediaWiki\Revision\SlotRecord;
17use MediaWiki\Title\NamespaceInfo;
18use MediaWiki\Title\Title;
19use MediaWiki\Title\TitleFactory;
20use MediaWiki\User\UserIdentity;
21use TextContent;
22use WikiImporterFactory;
23
24/**
25 * Service to import a translatable bundle from a file. Uses WikiImporter from MediaWiki core.
26 * @since 2023.05
27 * @license GPL-2.0-or-later
28 * @author Abijeet Patro
29 */
30class TranslatableBundleImporter implements AfterImportPageHook {
31    private WikiImporterFactory $wikiImporterFactory;
32    private TranslatablePageParser $translatablePageParser;
33    private RevisionLookup $revisionLookup;
34    private ?Title $bundleTitle;
35    private ?Closure $pageImportCompleteCallback = null;
36    private NamespaceInfo $namespaceInfo;
37    private TitleFactory $titleFactory;
38    private bool $importInProgress = false;
39
40    public function __construct(
41        WikiImporterFactory $wikiImporterFactory,
42        TranslatablePageParser $translatablePageParser,
43        RevisionLookup $revisionLookup,
44        NamespaceInfo $namespaceInfo,
45        TitleFactory $titleFactory
46    ) {
47        $this->wikiImporterFactory = $wikiImporterFactory;
48        $this->translatablePageParser = $translatablePageParser;
49        $this->revisionLookup = $revisionLookup;
50        $this->namespaceInfo = $namespaceInfo;
51        $this->titleFactory = $titleFactory;
52    }
53
54    /** Factory method used to initialize this HookHandler */
55    public static function getInstance(): self {
56        return Services::getInstance()->getTranslatableBundleImporter();
57    }
58
59    public function import(
60        string $importFilePath,
61        string $interwikiPrefix,
62        bool $assignKnownUsers,
63        UserIdentity $user,
64        ?Title $targetPage,
65        ?string $comment
66    ): Title {
67        $importSource = ImportStreamSource::newFromFile( $importFilePath );
68        if ( !$importSource->isOK() ) {
69            throw new TranslatableBundleImportException(
70                "Error while reading import file '$importFilePath': " . $importSource->getMessage()->text()
71            );
72        }
73
74        $wikiImporter = $this->wikiImporterFactory
75            // This is used only in a maintenance script (importTranslatableBundle.php),
76            // so use UltimateAuthority to skip permission checks
77            ->getWikiImporter( $importSource->value, new UltimateAuthority( $user ) );
78        $wikiImporter->setUsernamePrefix( $interwikiPrefix, $assignKnownUsers );
79
80        if ( $targetPage !== null ) {
81            $wikiImporter->setImportTitleFactory(
82                new TranslatableBundleImportTitleFactory( $this->namespaceInfo, $this->titleFactory, $targetPage )
83            );
84        }
85
86        try {
87            $this->importInProgress = true;
88            // Reset the currently set title which might have been set during the previous import process
89            $this->bundleTitle = null;
90            $importResult = $wikiImporter->doImport();
91        } catch ( Exception $e ) {
92            throw new TranslatableBundleImportException(
93                $e->getMessage(),
94                $e->getCode(),
95                $e
96            );
97        } finally {
98            $this->importInProgress = false;
99        }
100
101        if ( $importResult === false ) {
102            throw new TranslatableBundleImportException( 'Unknown error while importing translatable bundle.' );
103        }
104
105        if ( !$this->bundleTitle ) {
106            throw new TranslatableBundleImportException( 'Import done, but could not identify imported page.' );
107        }
108
109        // WikiImporter does not trigger hooks that run after a page is edited. Hence, manually add the ready
110        // tag to the imported page if it contains the markup
111        $this->addReadyTagForTranslatablePage( $this->bundleTitle );
112        $this->logImport( $user, $this->bundleTitle, $comment );
113
114        return $this->bundleTitle;
115    }
116
117    public function setPageImportCompleteCallback( callable $callable ): void {
118        $this->pageImportCompleteCallback = Closure::fromCallable( $callable );
119    }
120
121    private function logImport( UserIdentity $user, Title $bundle, ?string $comment ): void {
122        $entry = new ManualLogEntry( 'import', 'translatable-bundle' );
123        $entry->setPerformer( $user );
124        $entry->setTarget( $bundle );
125        $logId = $entry->insert();
126        if ( $comment ) {
127            $entry->setComment( $comment );
128        }
129        $entry->publish( $logId );
130    }
131
132    /** Add ready tag in case the page imported has <translate> markup */
133    private function addReadyTagForTranslatablePage( Title $translatablePageTitle ) {
134        $revisionRecord = $this->revisionLookup->getRevisionByTitle( $translatablePageTitle );
135        if ( !$revisionRecord ) {
136            throw new TranslatableBundleImportException(
137                "Revision record could not be found for imported page: $translatablePageTitle"
138            );
139        }
140
141        $content = $revisionRecord->getContent( SlotRecord::MAIN );
142        if ( !$content instanceof TextContent ) {
143            throw new TranslatableBundleImportException(
144                "Content in revision record for $translatablePageTitle is not of type TextContent"
145            );
146        }
147
148        if ( $this->translatablePageParser->containsMarkup( $content->getText() ) ) {
149            // Add the ready tag
150            $page = TranslatablePage::newFromTitle( Title::newFromLinkTarget( $translatablePageTitle ) );
151            $page->addReadyTag( $revisionRecord->getId() );
152        }
153    }
154
155    public function onAfterImportPage( $title, $foreignTitle, $revCount, $sRevCount, $pageInfo ) {
156        if ( $this->importInProgress ) {
157            $this->bundleTitle ??= $title;
158            if ( $this->pageImportCompleteCallback ) {
159                call_user_func( $this->pageImportCompleteCallback, $title, $foreignTitle );
160            }
161        }
162    }
163}