Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ImportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
6use BagOStuff;
11use MediaWiki\Html\Html;
12use MediaWiki\SpecialPage\SpecialPage;
14use Xml;
15
25class 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
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 {
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(), $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
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
144 private function outputForm(): void {
145 $this->getOutput()->addModules( 'ext.translate.special.importtranslations' );
146 $this->getOutput()->addHelpLink( 'Help:Extension:Translate/Off-line_translation' );
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 Xml::inputLabel(
158 $this->msg( 'translate-import-from-local' )->text(),
159 'upload-local', // name
160 'mw-translate-up-local-input', // id
161 50, // size
162 $this->getRequest()->getText( 'upload-local' ),
163 [ 'type' => 'file' ]
164 ) .
165 Xml::submitButton( $this->msg( 'translate-import-load' )->text() ) .
166 Xml::closeElement( 'form' )
167 );
168 }
169
171 private function loadFile( ?string &$filedata ): array {
172 $filename = $this->getRequest()->getFileTempname( 'upload-local' );
173
174 if ( !is_uploaded_file( $filename ) ) {
175 return [ 'ul-failed' ];
176 }
177
178 $filedata = file_get_contents( $filename );
179
180 return [ 'ok' ];
181 }
182
184 private function parseFile( string $data ): array {
189 $group = MessageGroupBase::factory( [
190 'FILES' => [
191 'format' => 'Gettext',
192 'CtxtAsKey' => true,
193 ],
194 'BASIC' => [
195 'class' => FileBasedMessageGroup::class,
196 'namespace' => -1,
197 ]
198 ] );
199 '@phan-var FileBasedMessageGroup $group';
200
201 $ffs = new GettextFormat( $group );
202
203 try {
204 $parseOutput = $ffs->readFromVariable( $data );
205 } catch ( GettextParseException $e ) {
206 return [ 'no-headers' ];
207 }
208
209 // Special data added by GettextFormat
210 $metadata = $parseOutput['EXTRA']['METADATA'];
211
212 // This should catch everything that is not a Gettext file exported from us
213 if ( !isset( $metadata['code'] ) || !isset( $metadata['group'] ) ) {
214 return [ 'no-headers' ];
215 }
216
217 return [ 'ok', $parseOutput ];
218 }
219
220 private function setCachedData( $data ): void {
221 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
222 $this->cache->set( $key, $data, 60 * 30 );
223 }
224
225 private function getCachedData() {
226 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
227
228 return $this->cache->get( $key );
229 }
230
231 private function deleteCachedData(): bool {
232 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
233
234 return $this->cache->delete( $key );
235 }
236}
This class implements default behavior for file based message groups.
FileFormat class that implements support for gettext file format.
Exception thrown when a Gettext file could not be parsed, such as when missing required headers.
Factory class for accessing message groups individually by id or all of them as a list.
Special page to import Gettext (.po) files exported using Translate extension.
This class implements some basic functions that wrap around the YAML message group configurations.
Finds external changes for file based message groups.