Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
ImportTranslationsSpecialPage.php
1<?php
2declare( strict_types = 1 );
3
5
6use BagOStuff;
8use GettextFFS;
10use 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 $group = $data['EXTRA']['METADATA']['group'];
99 $code = $data['EXTRA']['METADATA']['code'];
100
101 if ( !MessageGroups::exists( $group ) ) {
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(), $group, $code );
109 $importer->setUser( $this->getUser() );
110 $alldone = $importer->execute( $messages );
111
112 if ( $alldone ) {
113 $this->deleteCachedData();
114 }
115 }
116
122 private function checkError( array $msg ): bool {
123 // Give grep a chance to find the usages:
124 // translate-import-err-dl-failed, translate-import-err-ul-failed,
125 // translate-import-err-invalid-title, translate-import-err-no-such-file,
126 // translate-import-err-stale-group, translate-import-err-no-headers,
127 if ( $msg[0] !== 'ok' ) {
128 $errorWrap = "<div class='error'>\n$1\n</div>";
129 $msg[0] = 'translate-import-err-' . $msg[0];
130 $this->getOutput()->wrapWikiMsg( $errorWrap, $msg );
131 $this->outputForm();
132
133 return true;
134 }
135
136 return false;
137 }
138
140 private function outputForm(): void {
141 $this->getOutput()->addModules( 'ext.translate.special.importtranslations' );
142 $this->getOutput()->addHelpLink( 'Help:Extension:Translate/Off-line_translation' );
144 $this->getOutput()->addHTML(
145 Xml::openElement( 'form', [
146 'action' => $this->getPageTitle()->getLocalURL(),
147 'method' => 'post',
148 'enctype' => 'multipart/form-data',
149 'id' => 'mw-translate-import',
150 ] ) .
151 Html::hidden( 'token', $this->getContext()->getCsrfTokenSet()->getToken() ) .
152 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
153 Xml::inputLabel(
154 $this->msg( 'translate-import-from-local' )->text(),
155 'upload-local', // name
156 'mw-translate-up-local-input', // id
157 50, // size
158 $this->getRequest()->getText( 'upload-local' ),
159 [ 'type' => 'file' ]
160 ) .
161 Xml::submitButton( $this->msg( 'translate-import-load' )->text() ) .
162 Xml::closeElement( 'form' )
163 );
164 }
165
167 private function loadFile( ?string &$filedata ): array {
168 $filename = $this->getRequest()->getFileTempname( 'upload-local' );
169
170 if ( !is_uploaded_file( $filename ) ) {
171 return [ 'ul-failed' ];
172 }
173
174 $filedata = file_get_contents( $filename );
175
176 return [ 'ok' ];
177 }
178
180 private function parseFile( string $data ): array {
185 $group = MessageGroupBase::factory( [
186 'FILES' => [
187 'class' => GettextFFS::class,
188 'CtxtAsKey' => true,
189 ],
190 'BASIC' => [
191 'class' => FileBasedMessageGroup::class,
192 'namespace' => -1,
193 ]
194 ] );
195 '@phan-var FileBasedMessageGroup $group';
196
197 $ffs = new GettextFFS( $group );
198
199 try {
200 $parseOutput = $ffs->readFromVariable( $data );
201 } catch ( GettextParseException $e ) {
202 return [ 'no-headers' ];
203 }
204
205 // Special data added by GettextFFS
206 $metadata = $parseOutput['EXTRA']['METADATA'];
207
208 // This should catch everything that is not a Gettext file exported from us
209 if ( !isset( $metadata['code'] ) || !isset( $metadata['group'] ) ) {
210 return [ 'no-headers' ];
211 }
212
213 return [ 'ok', $parseOutput ];
214 }
215
216 private function setCachedData( $data ): void {
217 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
218 $this->cache->set( $key, $data, 60 * 30 );
219 }
220
221 private function getCachedData() {
222 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
223
224 return $this->cache->get( $key );
225 }
226
227 private function deleteCachedData(): bool {
228 $key = $this->cache->makeKey( 'translate', 'webimport', $this->getUser()->getId() );
229
230 return $this->cache->delete( $key );
231 }
232}
This class implements default behavior for file based message groups.
New-style FFS 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.