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