Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 104 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
ImportTranslationsSpecialPage | |
0.00% |
0 / 104 |
|
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 / 22 |
|
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 BagOStuff; |
7 | use FileBasedMessageGroup; |
8 | use MediaWiki\Extension\Translate\FileFormatSupport\GettextFormat; |
9 | use MediaWiki\Extension\Translate\FileFormatSupport\GettextParseException; |
10 | use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups; |
11 | use MediaWiki\Html\Html; |
12 | use MessageGroupBase; |
13 | use SpecialPage; |
14 | use 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 | */ |
25 | class 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 | } |