Overhauled language import form
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / language / LanguageEditor.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\data\language;
11ade432
AE
3use wcf\data\language\category\LanguageCategory;
4use wcf\data\language\category\LanguageCategoryEditor;
5use wcf\data\language\item\LanguageItemEditor;
6use wcf\data\language\item\LanguageItemList;
931f6597 7use wcf\data\DatabaseObjectEditor;
42c2d229 8use wcf\data\IEditableCachedObject;
b401cd0d 9use wcf\system\cache\builder\LanguageCacheBuilder;
11ade432 10use wcf\system\database\util\PreparedStatementConditionBuilder;
6286572b 11use wcf\system\exception\SystemException;
3b6c3ca4 12use wcf\system\io\AtomicWriter;
11ade432 13use wcf\system\language\LanguageFactory;
ac9b0f6e 14use wcf\system\Regex;
11ade432 15use wcf\system\WCF;
25e5c0cf 16use wcf\util\DirectoryUtil;
37a3683e 17use wcf\util\FileUtil;
6286572b
AE
18use wcf\util\StringUtil;
19use wcf\util\XML;
11ade432
AE
20
21/**
22 * Provides functions to edit languages.
a17de04e 23 *
11ade432 24 * @author Alexander Ebert
4bef7e27 25 * @copyright 2001-2016 WoltLab GmbH
11ade432
AE
26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
27 * @package com.woltlab.wcf
28 * @subpackage data.language
9f959ced 29 * @category Community Framework
4bef7e27
MS
30 *
31 * @method Language getDecoratedObject()
32 * @mixin Language
11ade432 33 */
42c2d229 34class LanguageEditor extends DatabaseObjectEditor implements IEditableCachedObject {
11ade432 35 /**
4bef7e27 36 * @inheritDoc
11ade432 37 */
4bef7e27 38 protected static $baseClass = Language::class;
11ade432
AE
39
40 /**
4bef7e27 41 * @inheritDoc
a17de04e 42 */
11ade432
AE
43 public function delete() {
44 parent::delete();
45
46 self::deleteLanguageFiles($this->languageID);
47 }
48
49 /**
50 * Updates the language files for the given category.
a17de04e 51 *
4bef7e27 52 * @param LanguageCategory $languageCategory
11ade432 53 */
2d2cf979 54 public function updateCategory(LanguageCategory $languageCategory) {
4bef7e27 55 $this->writeLanguageFiles([$languageCategory->languageCategoryID]);
11ade432
AE
56 }
57
58 /**
59 * Write the languages files.
a17de04e 60 *
7a23a706 61 * @param integer[] $languageCategoryIDs
11ade432 62 */
2d2cf979 63 protected function writeLanguageFiles(array $languageCategoryIDs) {
11ade432 64 $conditions = new PreparedStatementConditionBuilder();
4bef7e27
MS
65 $conditions->add("languageID = ?", [$this->languageID]);
66 $conditions->add("languageCategoryID IN (?)", [$languageCategoryIDs]);
11ade432 67
2d2cf979 68 // get language items
d726f13d 69 $sql = "SELECT languageItem, languageItemValue, languageCustomItemValue,
2d2cf979
AE
70 languageUseCustomValue, languageCategoryID
71 FROM wcf".WCF_N."_language_item
11ade432
AE
72 ".$conditions;
73 $statement = WCF::getDB()->prepareStatement($sql);
74 $statement->execute($conditions->getParameters());
4bef7e27 75 $items = [];
2d2cf979
AE
76 while ($row = $statement->fetchArray()) {
77 $languageCategoryID = $row['languageCategoryID'];
78 if (!isset($items[$languageCategoryID])) {
4bef7e27 79 $items[$languageCategoryID] = [];
2d2cf979 80 }
b8e1429f 81
2d2cf979
AE
82 $items[$languageCategoryID][$row['languageItem']] = ($row['languageUseCustomValue']) ? $row['languageCustomItemValue'] : $row['languageItemValue'];
83 }
84
85 foreach ($items as $languageCategoryID => $languageItems) {
86 $category = LanguageFactory::getInstance()->getCategoryByID($languageCategoryID);
87 if ($category === null) {
88 continue;
b8e1429f
AE
89 }
90
3b6c3ca4
TD
91 $filename = WCF_DIR.'language/'.$this->languageID.'_'.$category->languageCategory.'.php';
92 $writer = new AtomicWriter($filename);
93
94 $writer->write("<?php\n/**\n* WoltLab Community Framework\n* language: ".$this->languageCode."\n* encoding: UTF-8\n* category: ".$category->languageCategory."\n* generated at ".gmdate("r")."\n* \n* DO NOT EDIT THIS FILE\n*/\n");
2d2cf979 95 foreach ($languageItems as $languageItem => $languageItemValue) {
3b6c3ca4 96 $writer->write("\$this->items['".$languageItem."'] = '".str_replace("'", "\'", $languageItemValue)."';\n");
11ade432 97
2d2cf979
AE
98 // compile dynamic language variables
99 if ($category->languageCategory != 'wcf.global' && strpos($languageItemValue, '{') !== false) {
3b6c3ca4
TD
100 $writer->write("\$this->dynamicItems['".$languageItem."'] = '");
101
2d2cf979 102 $output = LanguageFactory::getInstance()->getScriptingCompiler()->compileString($languageItem, $languageItemValue);
3b6c3ca4
TD
103 $writer->write(str_replace("'", "\'", $output['template']));
104
105 $writer->write("';\n");
11ade432
AE
106 }
107 }
2d2cf979 108
3b6c3ca4
TD
109 $writer->flush();
110 $writer->close();
1232bce2 111 FileUtil::makeWritable($filename);
11ade432
AE
112 }
113 }
114
23af949d
JML
115 /**
116 * Exports this language.
6f37a5f5
MS
117 *
118 * @param integer[] $packageIDArray
119 * @param boolean $exportCustomValues
23af949d 120 */
4bef7e27 121 public function export($packageIDArray = [], $exportCustomValues = false) {
23af949d 122 $conditions = new PreparedStatementConditionBuilder();
4bef7e27 123 $conditions->add("language_item.languageID = ?", [$this->languageID]);
23af949d
JML
124
125 // bom
126 echo "\xEF\xBB\xBF";
127
128 // header
da13ec66 129 echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<language xmlns=\"http://www.woltlab.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd\" languagecode=\"".$this->languageCode."\" languagename=\"".$this->languageName."\" countrycode=\"".$this->countryCode."\">\n";
23af949d
JML
130
131 // get items
4bef7e27 132 $items = [];
15fa2802 133 if (!empty($packageIDArray)) {
4bef7e27 134 $conditions->add("language_item.packageID IN (?)", [$packageIDArray]);
23af949d 135 }
cba29a87 136
f1c1fc65
AE
137 $sql = "SELECT languageItem, " . ($exportCustomValues ? "CASE WHEN languageUseCustomValue > 0 THEN languageCustomItemValue ELSE languageItemValue END AS languageItemValue" : "languageItemValue") . ", languageCategory
138 FROM wcf".WCF_N."_language_item language_item
139 LEFT JOIN wcf".WCF_N."_language_category language_category
140 ON (language_category.languageCategoryID = language_item.languageCategoryID)
141 ".$conditions;
23af949d
JML
142 $statement = WCF::getDB()->prepareStatement($sql);
143 $statement->execute($conditions->getParameters());
144 while ($row = $statement->fetchArray()) {
145 $items[$row['languageCategory']][$row['languageItem']] = $row['languageItemValue'];
146 }
147
148 // sort categories
149 ksort($items);
150
151 foreach ($items as $category => $categoryItems) {
152 // sort items
153 ksort($categoryItems);
154
155 // category header
156 echo "\t<category name=\"".$category."\">\n";
157
158 // items
159 foreach ($categoryItems as $item => $value) {
160 echo "\t\t<item name=\"".$item."\"><![CDATA[".StringUtil::escapeCDATA($value)."]]></item>\n";
161 }
162
163 // category footer
164 echo "\t</category>\n";
165 }
166
167 // footer
168 echo "</language>";
169 }
170
11ade432
AE
171 /**
172 * Imports language items from an XML file into this language.
173 * Updates the relevant language files automatically.
a17de04e 174 *
4bef7e27 175 * @param XML $xml
11ade432
AE
176 * @param integer $packageID
177 * @param boolean $updateFiles
6b4766db 178 * @param boolean $updateExistingItems
11ade432 179 */
6b4766db 180 public function updateFromXML(XML $xml, $packageID, $updateFiles = true, $updateExistingItems = true) {
11ade432 181 $xpath = $xml->xpath();
4bef7e27 182 $usedCategories = [];
11ade432
AE
183
184 // fetch categories
185 $categories = $xpath->query('/ns:language/ns:category');
186 foreach ($categories as $category) {
187 $usedCategories[$category->getAttribute('name')] = 0;
188 }
189
15fa2802 190 if (empty($usedCategories)) return;
11ade432
AE
191
192 // select existing categories
193 $conditions = new PreparedStatementConditionBuilder();
4bef7e27 194 $conditions->add("languageCategory IN (?)", [array_keys($usedCategories)]);
11ade432
AE
195
196 $sql = "SELECT languageCategoryID, languageCategory
197 FROM wcf".WCF_N."_language_category
198 ".$conditions;
199 $statement = WCF::getDB()->prepareStatement($sql);
200 $statement->execute($conditions->getParameters());
201 while ($row = $statement->fetchArray()) {
202 $usedCategories[$row['languageCategory']] = $row['languageCategoryID'];
203 }
204
205 // create new categories
206 foreach ($usedCategories as $categoryName => $categoryID) {
207 if ($categoryID) continue;
208
4bef7e27 209 $category = LanguageCategoryEditor::create([
11ade432 210 'languageCategory' => $categoryName
4bef7e27 211 ]);
11ade432
AE
212 $usedCategories[$categoryName] = $category->languageCategoryID;
213 }
214
215 // loop through categories to import items
4bef7e27 216 $itemData = [];
11ade432
AE
217 foreach ($categories as $category) {
218 $categoryName = $category->getAttribute('name');
219 $categoryID = $usedCategories[$categoryName];
220
221 // loop through items
222 $elements = $xpath->query('child::*', $category);
223 foreach ($elements as $element) {
224 $itemName = $element->getAttribute('name');
225 $itemValue = $element->nodeValue;
226
3663a153
AE
227 $itemData[] = $this->languageID;
228 $itemData[] = $itemName;
229 $itemData[] = $itemValue;
230 $itemData[] = $categoryID;
6da165c5 231 if ($packageID) $itemData[] = ($packageID == -1) ? PACKAGE_ID : $packageID;
11ade432
AE
232 }
233 }
234
3663a153
AE
235 if (!empty($itemData)) {
236 // insert/update a maximum of 50 items per run (prevents issues with max_allowed_packet)
237 $step = ($packageID) ? 5 : 4;
6b4766db 238 WCF::getDB()->beginTransaction();
3663a153
AE
239 for ($i = 0, $length = count($itemData); $i < $length; $i += 50 * $step) {
240 $parameters = array_slice($itemData, $i, 50 * $step);
241 $repeat = count($parameters) / $step;
242
6b4766db 243 $sql = "INSERT".(!$updateExistingItems ? " IGNORE" : "")." INTO wcf".WCF_N."_language_item
8aea75d5 244 (languageID, languageItem, languageItemValue, languageCategoryID". ($packageID ? ", packageID" : "") . ")
6b4766db
AE
245 VALUES ".substr(str_repeat('(?, ?, ?, ?'. ($packageID ? ', ?' : '') .'), ', $repeat), 0, -2);
246
247 if ($updateExistingItems) {
6da165c5 248 if ($packageID > 0) {
5de7fea7
AE
249 // do not update anything if language item is owned by a different package
250 $sql .= " ON DUPLICATE KEY
251 UPDATE languageUseCustomValue = IF(packageID = ".$packageID.", IF(languageItemValue = VALUES(languageItemValue), languageUseCustomValue, 0), languageUseCustomValue),
252 languageItemValue = IF(packageID = ".$packageID.", IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)), languageItemValue),
253 languageCategoryID = IF(packageID = ".$packageID.", VALUES(languageCategoryID), languageCategoryID)";
254 }
255 else {
6da165c5 256 // skip package id check during WCFSetup (packageID = 0) or if using the ACP form (packageID = -1)
5de7fea7
AE
257 $sql .= " ON DUPLICATE KEY
258 UPDATE languageUseCustomValue = IF(languageItemValue = VALUES(languageItemValue), languageUseCustomValue, 0),
259 languageItemValue = IF(languageItemOriginIsSystem = 0, languageItemValue, VALUES(languageItemValue)),
260 languageCategoryID = VALUES(languageCategoryID)";
261 }
6b4766db
AE
262 }
263
3663a153
AE
264 $statement = WCF::getDB()->prepareStatement($sql);
265 $statement->execute($parameters);
11ade432 266 }
6b4766db 267 WCF::getDB()->commitTransaction();
11ade432
AE
268 }
269
270 // update the relevant language files
271 if ($updateFiles) {
272 self::deleteLanguageFiles($this->languageID);
273 }
274
275 // delete relevant template compilations
276 $this->deleteCompiledTemplates();
277 }
278
279 /**
280 * Deletes the language cache.
a17de04e 281 *
39bea7dd
MS
282 * @param string $languageID
283 * @param string $category
11ade432 284 */
e730016d 285 public static function deleteLanguageFiles($languageID = '.*', $category = '.*') {
25e5c0cf
AE
286 if ($category != '.*') $category = preg_quote($category, '~');
287 if ($languageID != '.*') $languageID = intval($languageID);
e3369fd2 288
e730016d 289 DirectoryUtil::getInstance(WCF_DIR.'language/')->removePattern(new Regex($languageID.'_'.$category.'\.php$'));
11ade432
AE
290 }
291
292 /**
293 * Deletes relevant template compilations.
294 */
295 public function deleteCompiledTemplates() {
296 // templates
ac9b0f6e 297 DirectoryUtil::getInstance(WCF_DIR.'templates/compiled/')->removePattern(new Regex('.*_'.$this->languageID.'_.*\.php$'));
11ade432 298 // acp templates
ac9b0f6e 299 DirectoryUtil::getInstance(WCF_DIR.'acp/templates/compiled/')->removePattern(new Regex('.*_'.$this->languageID.'_.*\.php$'));
11ade432
AE
300 }
301
302 /**
303 * Updates all language files of the given package id.
304 */
305 public static function updateAll() {
306 self::deleteLanguageFiles();
307 }
308
309 /**
310 * Takes an XML object and returns the specific language code.
a17de04e 311 *
4bef7e27 312 * @param XML $xml
a17de04e 313 * @return string
2b770bdd 314 * @throws SystemException
11ade432
AE
315 */
316 public static function readLanguageCodeFromXML(XML $xml) {
317 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
318 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
319 foreach ($attributes as $attribute) {
320 if ($attribute->name == 'languagecode') {
321 return $attribute->value;
322 }
323 }
324
4fe0b42b 325 throw new SystemException("missing attribute 'languagecode' in language file");
11ade432
AE
326 }
327
a74df36b
MW
328 /**
329 * Takes an XML object and returns the specific language name.
a17de04e 330 *
4bef7e27 331 * @param XML $xml
a74df36b 332 * @return string language name
2b770bdd 333 * @throws SystemException
a74df36b
MW
334 */
335 public static function readLanguageNameFromXML(XML $xml) {
336 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
337 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
338 foreach ($attributes as $attribute) {
339 if ($attribute->name == 'languagename') {
340 return $attribute->value;
341 }
342 }
343
344 throw new SystemException("missing attribute 'languagename' in language file");
345 }
346
6675b340
AE
347 /**
348 * Takes an XML object and returns the specific country code.
a17de04e 349 *
4bef7e27 350 * @param XML $xml
6675b340 351 * @return string country code
2b770bdd 352 * @throws SystemException
6675b340
AE
353 */
354 public static function readCountryCodeFromXML(XML $xml) {
355 $rootNode = $xml->xpath()->query('/ns:language')->item(0);
356 $attributes = $xml->xpath()->query('attribute::*', $rootNode);
357 foreach ($attributes as $attribute) {
358 if ($attribute->name == 'countrycode') {
359 return $attribute->value;
360 }
361 }
362
363 throw new SystemException("missing attribute 'countrycode' in language file");
364 }
365
11ade432
AE
366 /**
367 * Imports language items from an XML file into a new or a current language.
368 * Updates the relevant language files automatically.
a17de04e 369 *
d00afef8 370 * @param XML $xml
11ade432 371 * @param integer $packageID
d00afef8 372 * @param Language $source
4bef7e27 373 * @return LanguageEditor
11ade432 374 */
d00afef8 375 public static function importFromXML(XML $xml, $packageID, Language $source = null) {
11ade432
AE
376 $languageCode = self::readLanguageCodeFromXML($xml);
377
378 // try to find an existing language with the given language code
61022658 379 $language = LanguageFactory::getInstance()->getLanguageByCode($languageCode);
11ade432
AE
380
381 // create new language
382 if ($language === null) {
6675b340 383 $countryCode = self::readCountryCodeFromXML($xml);
a74df36b 384 $languageName = self::readLanguageNameFromXML($xml);
4bef7e27 385 $language = self::create([
6675b340 386 'countryCode' => $countryCode,
a74df36b
MW
387 'languageCode' => $languageCode,
388 'languageName' => $languageName
4bef7e27 389 ]);
d00afef8
MW
390
391 if ($source) {
392 $sourceEditor = new LanguageEditor($source);
393 $sourceEditor->copy($language);
394 }
11ade432
AE
395 }
396
397 // import xml
398 $languageEditor = new LanguageEditor($language);
399 $languageEditor->updateFromXML($xml, $packageID);
400
401 // return language object
402 return $languageEditor;
403 }
404
405 /**
406 * Copies all language variables from current language to language specified as $destination.
407 * Caution: This method expects that target language does not have any items!
408 *
73df94ae 409 * @param Language $destination
a17de04e 410 */
11ade432
AE
411 public function copy(Language $destination) {
412 $sql = "INSERT INTO wcf".WCF_N."_language_item
413 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
414 SELECT ?, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID
415 FROM wcf".WCF_N."_language_item
416 WHERE languageID = ?";
417 $statement = WCF::getDB()->prepareStatement($sql);
4bef7e27 418 $statement->execute([
11ade432
AE
419 $destination->languageID,
420 $this->languageID
4bef7e27 421 ]);
11ade432
AE
422 }
423
424 /**
425 * Updates the language items of a language category.
426 *
4bef7e27
MS
427 * @param array $items
428 * @param LanguageCategory $category
429 * @param integer $packageID
430 * @param array $useCustom
11ade432 431 */
4bef7e27 432 public function updateItems(array $items, LanguageCategory $category, $packageID = PACKAGE_ID, array $useCustom = []) {
15fa2802 433 if (empty($items)) return;
11ade432
AE
434
435 // find existing language items
436 $languageItemList = new LanguageItemList();
4bef7e27
MS
437 $languageItemList->getConditionBuilder()->add("language_item.languageItem IN (?)", [array_keys($items)]);
438 $languageItemList->getConditionBuilder()->add("languageID = ?", [$this->languageID]);
11ade432
AE
439 $languageItemList->readObjects();
440
58e1d71f 441 foreach ($languageItemList->getObjects() as $languageItem) {
11ade432 442 $languageItemEditor = new LanguageItemEditor($languageItem);
4bef7e27 443 $languageItemEditor->update([
11ade432
AE
444 'languageCustomItemValue' => $items[$languageItem->languageItem],
445 'languageUseCustomValue' => (isset($useCustom[$languageItem->languageItem])) ? 1 : 0
4bef7e27 446 ]);
11ade432
AE
447
448 // remove updated items, leaving items to be created within
449 unset($items[$languageItem->languageItem]);
450 }
451
452 // create remaining items
15fa2802 453 if (!empty($items)) {
11ade432
AE
454 // bypass LanguageItemEditor::create() for performance reasons
455 $sql = "INSERT INTO wcf".WCF_N."_language_item
456 (languageID, languageItem, languageItemValue, languageItemOriginIsSystem, languageCategoryID, packageID)
457 VALUES (?, ?, ?, ?, ?, ?)";
458 $statement = WCF::getDB()->prepareStatement($sql);
459
460 foreach ($items as $itemName => $itemValue) {
4bef7e27 461 $statement->execute([
11ade432
AE
462 $this->languageID,
463 $itemName,
464 $itemValue,
465 0,
466 $category->languageCategoryID,
467 $packageID
4bef7e27 468 ]);
11ade432
AE
469 }
470 }
471
472 // update the relevant language files
4879676b 473 self::deleteLanguageFiles($this->languageID, $category->languageCategory);
11ade432
AE
474
475 // delete relevant template compilations
476 $this->deleteCompiledTemplates();
477 }
478
479 /**
480 * Sets current language as default language.
481 */
482 public function setAsDefault() {
483 // remove default flag from all languages
484 $sql = "UPDATE wcf".WCF_N."_language
485 SET isDefault = ?";
486 $statement = WCF::getDB()->prepareStatement($sql);
4bef7e27 487 $statement->execute([0]);
11ade432
AE
488
489 // set current language as default language
4bef7e27 490 $this->update(['isDefault' => 1]);
11ade432
AE
491
492 $this->clearCache();
493 }
494
495 /**
496 * Clears language cache.
a17de04e 497 */
11ade432 498 public function clearCache() {
b401cd0d 499 LanguageCacheBuilder::getInstance()->reset();
11ade432
AE
500 }
501
11ade432
AE
502 /**
503 * Enables the multilingualism feature for given languages.
504 *
505 * @param array $languageIDs
506 */
4bef7e27 507 public static function enableMultilingualism(array $languageIDs = []) {
11ade432
AE
508 $sql = "UPDATE wcf".WCF_N."_language
509 SET hasContent = ?";
510 $statement = WCF::getDB()->prepareStatement($sql);
4bef7e27 511 $statement->execute([0]);
11ade432 512
15fa2802 513 if (!empty($languageIDs)) {
11ade432 514 $sql = '';
4bef7e27 515 $statementParameters = [];
11ade432
AE
516 foreach ($languageIDs as $languageID) {
517 if (!empty($sql)) $sql .= ',';
518 $sql .= '?';
519 $statementParameters[] = $languageID;
520 }
521
522 $sql = "UPDATE wcf".WCF_N."_language
523 SET hasContent = ?
524 WHERE languageID IN (".$sql.")";
525 $statement = WCF::getDB()->prepareStatement($sql);
526 array_unshift($statementParameters, 1);
527 $statement->execute($statementParameters);
528 }
529 }
42c2d229
MS
530
531 /**
4bef7e27 532 * @inheritDoc
42c2d229
MS
533 */
534 public static function resetCache() {
535 LanguageFactory::getInstance()->clearCache();
536 }
11ade432 537}