Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ImportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
6use BagOStuff;
8use Html;
13use SpecialPage;
14use Xml;
15
25class ImportTranslationsSpecialPage extends SpecialPage {
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
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 {
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
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
145 private function outputForm(): void {
146 $this->getOutput()->addModules( 'ext.translate.special.importtranslations' );
147 $this->getOutput()->addHelpLink( 'Help:Extension:Translate/Off-line_translation' );
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
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
185 private function parseFile( string $data ): array {
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}
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.