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 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportTranslationsSpecialPage
0.00% covered (danger)
0.00%
0 / 104
0.00% covered (danger)
0.00%
0 / 11
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
110
 checkError
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 outputForm
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 loadFile
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 parseFile
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 setCachedData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCachedData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteCachedData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\Synchronization;
5
6use BagOStuff;
7use FileBasedMessageGroup;
8use MediaWiki\Extension\Translate\FileFormatSupport\GettextFormat;
9use MediaWiki\Extension\Translate\FileFormatSupport\GettextParseException;
10use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
11use MediaWiki\Html\Html;
12use MessageGroupBase;
13use SpecialPage;
14use Xml;
15
16/**
17 * Special page to import Gettext (.po) files exported using Translate extension.
18 * Does not support generic Gettext files.
19 *
20 * @author Niklas Laxström
21 * @author Siebrand Mazeland
22 * @license GPL-2.0-or-later
23 * @ingroup SpecialPage TranslateSpecialPage
24 */
25class ImportTranslationsSpecialPage extends SpecialPage {
26    /** @var BagOStuff */
27    private $cache;
28
29    public function __construct( BagOStuff $cache ) {
30        parent::__construct( 'ImportTranslations', 'translate-import' );
31        $this->cache = $cache;
32    }
33
34    public function doesWrites() {
35        return true;
36    }
37
38    protected function getGroupName() {
39        return 'translation';
40    }
41
42    /**
43     * Special page entry point.
44     * @param null|string $parameters
45     * @throws \PermissionsError
46     */
47    public function execute( $parameters ) {
48        $this->setHeaders();
49
50        // Security and validity checks
51        if ( !$this->userCanExecute( $this->getUser() ) ) {
52            $this->displayRestrictionError();
53        }
54
55        if ( !$this->getRequest()->wasPosted() ) {
56            $this->outputForm();
57
58            return;
59        }
60
61        $csrfTokenSet = $this->getContext()->getCsrfTokenSet();
62        if ( !$csrfTokenSet->matchTokenField( 'token' ) ) {
63            $this->getOutput()->addWikiMsg( 'session_fail_preview' );
64            $this->outputForm();
65
66            return;
67        }
68
69        if ( $this->getRequest()->getCheck( 'process' ) ) {
70            $data = $this->getCachedData();
71            if ( !$data ) {
72                $this->getOutput()->addWikiMsg( 'session_fail_preview' );
73                $this->outputForm();
74
75                return;
76            }
77        } else {
78            /**
79             * Proceed to loading and parsing if possible
80             * @todo: use a Status object instead?
81             */
82            $file = null;
83            $msg = $this->loadFile( $file );
84            if ( $this->checkError( $msg ) ) {
85                return;
86            }
87
88            $msg = $this->parseFile( $file );
89            if ( $this->checkError( $msg ) ) {
90                return;
91            }
92
93            $data = $msg[1];
94            $this->setCachedData( $data );
95        }
96
97        $messages = $data['MESSAGES'];
98        $groupId = $data['EXTRA']['METADATA']['group'];
99        $code = $data['EXTRA']['METADATA']['code'];
100
101        if ( !MessageGroups::exists( $groupId ) ) {
102            $errorWrap = "<div class='error'>\n$1\n</div>";
103            $this->getOutput()->wrapWikiMsg( $errorWrap, 'translate-import-err-stale-group' );
104
105            return;
106        }
107
108        $importer = new MessageWebImporter( $this->getPageTitle(), $this->getUser(), $groupId, $code );
109        $allDone = $importer->execute( $messages );
110
111        $out = $this->getOutput();
112        $pageTitle = $this->getPageTitle();
113
114        if ( $allDone ) {
115            $this->deleteCachedData();
116            $this->outputForm();
117        }
118
119        $out->addBacklinkSubtitle( $pageTitle );
120    }
121
122    /**
123     * Checks for error state from the return value of loadFile and parseFile
124     * functions. Prints the error and the form and returns true if there is an
125     * error. Returns false and does nothing if there is no error.
126     */
127    private function checkError( array $msg ): bool {
128        // Give grep a chance to find the usages:
129        // translate-import-err-dl-failed, translate-import-err-ul-failed,
130        // translate-import-err-invalid-title, translate-import-err-no-such-file,
131        // translate-import-err-stale-group, translate-import-err-no-headers,
132        if ( $msg[0] !== 'ok' ) {
133            $errorWrap = "<div class='error'>\n$1\n</div>";
134            $msg[0] = 'translate-import-err-' . $msg[0];
135            $this->getOutput()->wrapWikiMsg( $errorWrap, $msg );
136            $this->outputForm();
137
138            return true;
139        }
140
141        return false;
142    }
143
144    /** Constructs and outputs file input form with supported methods. */
145    private function outputForm(): void {
146        $this->getOutput()->addModules( 'ext.translate.special.importtranslations' );
147        $this->getOutput()->addHelpLink( 'Help:Extension:Translate/Off-line_translation' );
148        /** Ugly but necessary form building ahead, ohoy */
149        $this->getOutput()->addHTML(
150            Xml::openElement( 'form', [
151                'action' => $this->getPageTitle()->getLocalURL(),
152                'method' => 'post',
153                'enctype' => 'multipart/form-data',
154                'id' => 'mw-translate-import',
155            ] ) .
156                Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
157                Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
158                Xml::inputLabel(
159                    $this->msg( 'translate-import-from-local' )->text(),
160                    'upload-local', // name
161                    'mw-translate-up-local-input', // id
162                    50, // size
163                    $this->getRequest()->getText( 'upload-local' ),
164                    [ 'type' => 'file' ]
165                ) .
166                Xml::submitButton( $this->msg( 'translate-import-load' )->text() ) .
167                Xml::closeElement( 'form' )
168        );
169    }
170
171    /** Try to get the file data from any of the supported methods. */
172    private function loadFile( ?string &$filedata ): array {
173        $filename = $this->getRequest()->getFileTempname( 'upload-local' );
174
175        if ( !is_uploaded_file( $filename ) ) {
176            return [ 'ul-failed' ];
177        }
178
179        $filedata = file_get_contents( $filename );
180
181        return [ 'ok' ];
182    }
183
184    /** Try parsing file. */
185    private function parseFile( string $data ): array {
186        /** Construct a dummy group for us...
187         * @todo Time to rethink the interface again?
188         * @var FileBasedMessageGroup $group
189         */
190        $group = MessageGroupBase::factory( [
191            'FILES' => [
192                'format' => 'Gettext',
193                'CtxtAsKey' => true,
194            ],
195            'BASIC' => [
196                'class' => FileBasedMessageGroup::class,
197                'namespace' => -1,
198            ]
199        ] );
200        '@phan-var FileBasedMessageGroup $group';
201
202        $ffs = new GettextFormat( $group );
203
204        try {
205            $parseOutput = $ffs->readFromVariable( $data );
206        } catch ( GettextParseException $e ) {
207            return [ 'no-headers' ];
208        }
209
210        // Special data added by GettextFormat
211        $metadata = $parseOutput['EXTRA']['METADATA'];
212
213        // This should catch everything that is not a Gettext file exported from us
214        if ( !isset( $metadata['code'] ) || !isset( $metadata['group'] ) ) {
215            return [ 'no-headers' ];
216        }
217
218        return [ 'ok', $parseOutput ];
219    }
220
221    private function setCachedData( $data ): void {
222        $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
223        $this->cache->set( $key, $data, 60 * 30 );
224    }
225
226    private function getCachedData() {
227        $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
228
229        return $this->cache->get( $key );
230    }
231
232    private function deleteCachedData(): bool {
233        $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
234
235        return $this->cache->delete( $key );
236    }
237}